mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-09 03:37:03 +01:00
Bangle.js: Refactor activity details
This commit is contained in:
parent
dc8d295d4a
commit
5730e82380
@ -0,0 +1,241 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.banglejs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GBToStringBuilder;
|
||||
|
||||
public class BangleJSActivityPoint {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BangleJSActivityPoint.class);
|
||||
|
||||
private final GPSCoordinate location;
|
||||
private final long time;
|
||||
private final int heartRate;
|
||||
private final int hrConfidence;
|
||||
private final String hrSource;
|
||||
private final int steps;
|
||||
private final int batteryPercentage;
|
||||
private final double batteryVoltage;
|
||||
private final boolean charging;
|
||||
private final double barometerTemperature;
|
||||
private final double barometerPressure;
|
||||
private final double barometerAltitude;
|
||||
|
||||
public BangleJSActivityPoint(final long time,
|
||||
final GPSCoordinate location,
|
||||
final int heartRate,
|
||||
final int hrConfidence,
|
||||
final String hrSource,
|
||||
final int steps,
|
||||
final int batteryPercentage,
|
||||
final double batteryVoltage,
|
||||
final boolean charging,
|
||||
final double barometerTemperature,
|
||||
final double barometerPressure,
|
||||
final double barometerAltitude) {
|
||||
this.time = time;
|
||||
this.location = location;
|
||||
this.heartRate = heartRate;
|
||||
this.hrConfidence = hrConfidence;
|
||||
this.hrSource = hrSource;
|
||||
this.steps = steps;
|
||||
this.batteryPercentage = batteryPercentage;
|
||||
this.batteryVoltage = batteryVoltage;
|
||||
this.charging = charging;
|
||||
this.barometerTemperature = barometerTemperature;
|
||||
this.barometerPressure = barometerPressure;
|
||||
this.barometerAltitude = barometerAltitude;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public GPSCoordinate getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public int getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
public int getHrConfidence() {
|
||||
return hrConfidence;
|
||||
}
|
||||
|
||||
public String getHrSource() {
|
||||
return hrSource;
|
||||
}
|
||||
|
||||
public int getSteps() {
|
||||
return steps;
|
||||
}
|
||||
|
||||
public int getBatteryPercentage() {
|
||||
return batteryPercentage;
|
||||
}
|
||||
|
||||
public double getBatteryVoltage() {
|
||||
return batteryVoltage;
|
||||
}
|
||||
|
||||
public boolean isCharging() {
|
||||
return charging;
|
||||
}
|
||||
|
||||
public double getBarometerTemperature() {
|
||||
return barometerTemperature;
|
||||
}
|
||||
|
||||
public double getBarometerPressure() {
|
||||
return barometerPressure;
|
||||
}
|
||||
|
||||
public double getBarometerAltitude() {
|
||||
return barometerAltitude;
|
||||
}
|
||||
|
||||
public ActivityPoint toActivityPoint() {
|
||||
final ActivityPoint activityPoint = new ActivityPoint();
|
||||
activityPoint.setTime(new Date(time));
|
||||
if (heartRate > 0) {
|
||||
activityPoint.setHeartRate(heartRate);
|
||||
}
|
||||
if (location != null) {
|
||||
activityPoint.setLocation(location);
|
||||
}
|
||||
return activityPoint;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
final GBToStringBuilder tsb = new GBToStringBuilder(this);
|
||||
tsb.append("location", location);
|
||||
tsb.append("time", time);
|
||||
tsb.append("heartRate", heartRate);
|
||||
tsb.append("hrConfidence", hrConfidence);
|
||||
tsb.append("hrSource", hrSource);
|
||||
tsb.append("steps", steps);
|
||||
tsb.append("batteryPercentage", batteryPercentage);
|
||||
tsb.append("batteryVoltage", batteryVoltage);
|
||||
tsb.append("charging", charging);
|
||||
tsb.append("barometerTemperature", barometerTemperature);
|
||||
tsb.append("barometerPressure", barometerPressure);
|
||||
tsb.append("barometerAltitude", barometerAltitude);
|
||||
return tsb.toString();
|
||||
}
|
||||
|
||||
public static List<BangleJSActivityPoint> fromCsv(final File inputFile) {
|
||||
final List<BangleJSActivityPoint> points = new LinkedList<>();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(inputFile))) {
|
||||
final List<String> header = Arrays.asList(reader.readLine().split(","));
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
points.add(BangleJSActivityPoint.fromCsvLine(header, line));
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
LOG.error("Failed to read {}", inputFile);
|
||||
return null;
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* This parses all the standard fields from the <a href="https://github.com/espruino/BangleApps/blob/master/apps/recorder/widget.js">recorder</a>.
|
||||
* Some apps such as bthrm add extra fields or modify others. We attempt to gracefully handle other formats (eg. source being "int" or "bthrm").
|
||||
*/
|
||||
@Nullable
|
||||
@VisibleForTesting
|
||||
public static BangleJSActivityPoint fromCsvLine(final List<String> header, final String csvLine) {
|
||||
final String[] split = csvLine.trim().replace(",", ", ").split(",");
|
||||
if (split.length != header.size()) {
|
||||
LOG.error("csv line {} length {} differs from header {} length {}", csvLine, split.length, header, header.size());
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < split.length; i++) {
|
||||
split[i] = split[i].strip();
|
||||
}
|
||||
|
||||
final int idxTime = header.indexOf("Time");
|
||||
final int idxLatitude = header.indexOf("Latitude");
|
||||
final int idxLongitude = header.indexOf("Longitude");
|
||||
final int idxAltitude = header.indexOf("Altitude");
|
||||
final int idxHeartrate = header.indexOf("Heartrate");
|
||||
final int idxConfidence = header.indexOf("Confidence");
|
||||
final int idxSource = header.indexOf("Source");
|
||||
final int idxSteps = header.indexOf("Steps");
|
||||
final int idxBatteryPercentage = header.indexOf("Battery Percentage");
|
||||
final int idxBatteryVoltage = header.indexOf("Battery Voltage");
|
||||
final int idxCharging = header.indexOf("Charging");
|
||||
final int idxBarometerTemperature = header.indexOf("Barometer Temperature");
|
||||
final int idxBarometerPressure = header.indexOf("Barometer Pressure");
|
||||
final int idxBarometerAltitude = header.indexOf("Barometer Altitude");
|
||||
|
||||
final long time = idxTime >= 0 && StringUtils.isNotBlank(split[idxTime]) ? ((long) (Double.parseDouble(split[idxTime]) * 1000L)) : 0L;
|
||||
|
||||
try {
|
||||
final GPSCoordinate location;
|
||||
if (idxLatitude >= 0 && StringUtils.isNotBlank(split[idxLatitude]) && idxLongitude >= 0 && StringUtils.isNotBlank(split[idxLongitude])) {
|
||||
final double latitude = Double.parseDouble(split[idxLatitude]);
|
||||
final double longitude = Double.parseDouble(split[idxLongitude]);
|
||||
final double altitude;
|
||||
if (idxAltitude >= 0 && StringUtils.isNotBlank(split[idxAltitude])) {
|
||||
altitude = Double.parseDouble(split[idxAltitude]);
|
||||
} else {
|
||||
altitude = GPSCoordinate.UNKNOWN_ALTITUDE;
|
||||
}
|
||||
location = new GPSCoordinate(longitude, latitude, altitude);
|
||||
} else {
|
||||
location = null;
|
||||
}
|
||||
final int heartRate = idxHeartrate >= 0 && StringUtils.isNotBlank(split[idxHeartrate]) ? (int) Math.round(Double.parseDouble(split[idxHeartrate])) : 0;
|
||||
final int confidence = idxConfidence >= 0 && StringUtils.isNotBlank(split[idxConfidence]) ? Integer.parseInt(split[idxConfidence]) : 0;
|
||||
final String source = idxSource >= 0 && StringUtils.isNotBlank(split[idxSource]) ? split[idxSource] : "";
|
||||
final int steps = idxSteps >= 0 && StringUtils.isNotBlank(split[idxSteps]) ? Integer.parseInt(split[idxSteps]) : 0;
|
||||
final int batteryPercentage = idxBatteryPercentage >= 0 && StringUtils.isNotBlank(split[idxBatteryPercentage]) ? Integer.parseInt(split[idxBatteryPercentage]) : -1;
|
||||
final double batteryVoltage = idxBatteryVoltage >= 0 && StringUtils.isNotBlank(split[idxBatteryVoltage]) ? Double.parseDouble(split[idxBatteryVoltage]) : -1;
|
||||
final boolean charging = idxCharging >= 0 && StringUtils.isNotBlank(split[idxCharging]) && Boolean.parseBoolean(split[idxCharging]);
|
||||
final double barometerTemperature = idxBarometerTemperature >= 0 && StringUtils.isNotBlank(split[idxBarometerTemperature]) ? Double.parseDouble(split[idxBarometerTemperature]) : 0;
|
||||
final double barometerPressure = idxBarometerPressure >= 0 && StringUtils.isNotBlank(split[idxBarometerPressure]) ? Double.parseDouble(split[idxBarometerPressure]) : 0;
|
||||
final double barometerAltitude = idxBarometerAltitude >= 0 && StringUtils.isNotBlank(split[idxBarometerAltitude]) ? Double.parseDouble(split[idxBarometerAltitude]) : GPSCoordinate.UNKNOWN_ALTITUDE;
|
||||
|
||||
return new BangleJSActivityPoint(
|
||||
time,
|
||||
location,
|
||||
heartRate,
|
||||
confidence,
|
||||
source,
|
||||
steps,
|
||||
batteryPercentage,
|
||||
batteryVoltage,
|
||||
charging,
|
||||
barometerTemperature,
|
||||
barometerPressure,
|
||||
barometerAltitude
|
||||
);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("failed to parse '{}'", csvLine, e);
|
||||
// Salvage the time at least
|
||||
return new BangleJSActivityPoint(time, null, 0, 0, "", 0, -1, -1, false, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import android.net.Uri;
|
||||
import android.os.ParcelUuid;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
@ -47,6 +48,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs.BangleJSDeviceSupport;
|
||||
@ -94,6 +96,12 @@ public class BangleJSCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return EnumSet.of(SleepAsAndroidFeature.ACCELEROMETER, SleepAsAndroidFeature.HEART_RATE, SleepAsAndroidFeature.NOTIFICATIONS, SleepAsAndroidFeature.ALARMS);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) {
|
||||
return new BangleJSWorkoutParser(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData() {
|
||||
return true;
|
||||
|
@ -0,0 +1,170 @@
|
||||
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<BangleJSActivityPoint> points = BangleJSActivityPoint.fromCsv(inputFile);
|
||||
if (points == null) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
summary.setSummaryData(dataFromPoints(points).toString());
|
||||
return summary;
|
||||
}
|
||||
|
||||
public static ActivitySummaryData dataFromPoints(final List<BangleJSActivityPoint> 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;
|
||||
}
|
||||
}
|
@ -34,11 +34,15 @@ public class ActivitySummaryData extends JSONObject {
|
||||
add(null, key, value, unit);
|
||||
}
|
||||
|
||||
public void add(final String key, final double value, final String unit) {
|
||||
add(null, key, value, unit);
|
||||
}
|
||||
|
||||
public void add(final String key, final String value) {
|
||||
add(null, key, value);
|
||||
}
|
||||
|
||||
public void add(final String group, final String key, final float value, final String unit) {
|
||||
public void add(final String group, final String key, final double value, final String unit) {
|
||||
if (value > 0) {
|
||||
try {
|
||||
final JSONObject innerData = new JSONObject();
|
||||
|
@ -1,9 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.banglejs;
|
||||
|
||||
import static java.lang.Integer.parseInt;
|
||||
import static java.lang.Long.parseLong;
|
||||
import static java.lang.Math.cos;
|
||||
import static java.lang.Math.sqrt;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_AVG;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.INTERNAL_HAS_GPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.SPEED_AVG;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
@ -15,14 +16,12 @@ import org.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
@ -30,6 +29,8 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.banglejs.BangleJSWorkoutParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
@ -39,9 +40,8 @@ import nodomain.freeyourgadget.gadgetbridge.export.GPXExporter;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
@ -158,241 +158,32 @@ class BangleJSActivityTrack {
|
||||
stopTimeoutTask(); // Parsing can take a while if there are many data. Restart at end of parsing.
|
||||
|
||||
File inputFile = new File(dir, filename);
|
||||
try { // FIXME: There is maybe code inside this try-statement that should be outside of it.
|
||||
try {
|
||||
BaseActivitySummary summary = new BaseActivitySummary();
|
||||
|
||||
// Read from the previously stored log into a string.
|
||||
BufferedReader reader = new BufferedReader(new FileReader(inputFile));
|
||||
StringBuilder storedLogBuilder = new StringBuilder(reader.readLine() + "\n");
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
storedLogBuilder.append(line).append("\n");
|
||||
final List<BangleJSActivityPoint> banglePoints = BangleJSActivityPoint.fromCsv(inputFile);
|
||||
if (banglePoints == null || banglePoints.isEmpty()) {
|
||||
// Should never happen?
|
||||
return;
|
||||
}
|
||||
reader.close();
|
||||
String storedLog = String.valueOf(storedLogBuilder);
|
||||
storedLog = storedLog.replace(",",", "); // So all rows (internal arrays) in storedLogArray2 get the same number of entries.
|
||||
LOG.debug("Contents of log read from GB storage:\n" + storedLog);
|
||||
|
||||
// Turn the string log into a 2d array in two steps.
|
||||
String[] storedLogArray = storedLog.split("\n") ;
|
||||
String[][] storedLogArray2 = new String[storedLogArray.length][1];
|
||||
|
||||
for (int i = 0; i < storedLogArray.length; i++) {
|
||||
storedLogArray2[i] = storedLogArray[i].split(",");
|
||||
for (int j = 0; j < storedLogArray2[i].length;j++) {
|
||||
storedLogArray2[i][j] = storedLogArray2[i][j].trim(); // Remove the extra spaces we introduced above for getting the same number of entries on all rows.
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug("Contents of storedLogArray2:\n" + Arrays.deepToString(storedLogArray2));
|
||||
|
||||
// Turn the 2d array into an object for easier access later on.
|
||||
JSONObject storedLogObject = new JSONObject();
|
||||
JSONArray valueArray = new JSONArray();
|
||||
for (int i = 0; i < storedLogArray2[0].length; i++){
|
||||
for (int j = 1; j < storedLogArray2.length; j++) {
|
||||
valueArray.put(storedLogArray2[j][i]);
|
||||
}
|
||||
storedLogObject.put(storedLogArray2[0][i], valueArray);
|
||||
valueArray = new JSONArray();
|
||||
}
|
||||
|
||||
// Clean out heartrate==0...
|
||||
if (storedLogObject.has("Heartrate")) {
|
||||
JSONArray heartrateArray = storedLogObject.getJSONArray("Heartrate");
|
||||
for (int i = 0; i < heartrateArray.length(); i++){
|
||||
if (Objects.equals(heartrateArray.getString(i), "0") ||
|
||||
Objects.equals(heartrateArray.getString(i), "0.0")) {
|
||||
heartrateArray.put(i,"");
|
||||
}
|
||||
}
|
||||
//storedLogObject.remove("Heartrate");
|
||||
storedLogObject.put("Heartrate", heartrateArray);
|
||||
|
||||
}
|
||||
|
||||
LOG.debug("storedLogObject:\n" + storedLogObject);
|
||||
|
||||
// Calculate and store analytical data (distance, speed, cadence, etc.).
|
||||
JSONObject analyticsObject = new JSONObject();
|
||||
JSONArray calculationsArray = new JSONArray();
|
||||
int logLength = storedLogObject.getJSONArray("Time").length();
|
||||
|
||||
// Add elapsed time since first reading (seconds).
|
||||
valueArray = storedLogObject.getJSONArray("Time");
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
calculationsArray.put(valueArray.getDouble(i)-valueArray.getDouble(0));
|
||||
}
|
||||
analyticsObject.put("Elapsed Time", calculationsArray);
|
||||
|
||||
valueArray = new JSONArray();
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
JSONArray valueArray2 = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 0");
|
||||
// Add analytics based on GPS coordinates.
|
||||
if (storedLogObject.has("Latitude")) {
|
||||
// Add distance between last and current reading.
|
||||
valueArray = storedLogObject.getJSONArray("Latitude");
|
||||
valueArray2 = storedLogObject.getJSONArray("Longitude");
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i == 0) {
|
||||
calculationsArray.put("0");
|
||||
} else {
|
||||
String distance;
|
||||
if (Objects.equals(valueArray.getString(i), "") ||
|
||||
Objects.equals(valueArray.getString(i - 1), "")) {
|
||||
// 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?
|
||||
distance = "0";
|
||||
} else {
|
||||
distance = distanceFromCoordinatePairs(
|
||||
(String) valueArray.get(i - 1),
|
||||
(String) valueArray2.get(i - 1),
|
||||
(String) valueArray.get(i),
|
||||
(String) valueArray2.get(i)
|
||||
);
|
||||
}
|
||||
calculationsArray.put(distance);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Intermediate Distance", calculationsArray);
|
||||
|
||||
valueArray = new JSONArray();
|
||||
valueArray2 = new JSONArray();
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 1");
|
||||
// Add stride lengths between consecutive readings.
|
||||
if (storedLogObject.has("Steps")) {
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "0") ||
|
||||
Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "")) {
|
||||
calculationsArray.put("");
|
||||
} else if (Objects.equals(analyticsObject.getJSONArray("Intermediate Distance").getString(i), "0")) {
|
||||
calculationsArray.put("0");
|
||||
} else {
|
||||
double steps = storedLogObject.getJSONArray("Steps").getDouble(i);
|
||||
double calculation =
|
||||
analyticsObject.getJSONArray("Intermediate Distance").getDouble(i) / steps;
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Stride", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
}
|
||||
|
||||
//LOG.debug("check here 2");
|
||||
} else if (storedLogObject.has("Steps")) {
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0 ||
|
||||
Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "0") ||
|
||||
Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "")) {
|
||||
calculationsArray.put(0);
|
||||
} else {
|
||||
double avgStep = (0.67+0.762)/2; // https://marathonhandbook.com/average-stride-length/ (female+male)/2
|
||||
double stride = 2*avgStep; // TODO: Depend on user defined stride length?
|
||||
double calculation = stride * (storedLogObject.getJSONArray("Steps").getDouble(i));
|
||||
//if (calculation == 0) calculation = 0.001; // To avoid potential division by zero later on.
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Intermediate Distance", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
}
|
||||
|
||||
//LOG.debug("check here 3");
|
||||
if (analyticsObject.has("Intermediate Distance")) {
|
||||
// Add total distance from start of activity up to each reading.
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0) {
|
||||
calculationsArray.put(0);
|
||||
} else {
|
||||
double calculation = calculationsArray.getDouble(i-1) + analyticsObject.getJSONArray("Intermediate Distance").getDouble(i);
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Total Distance", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 4");
|
||||
// Add average speed between last and current reading (m/s).
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0) {
|
||||
calculationsArray.put("");
|
||||
} else {
|
||||
double timeDiff =
|
||||
(analyticsObject.getJSONArray("Elapsed Time").getDouble(i) -
|
||||
analyticsObject.getJSONArray("Elapsed Time").getDouble(i-1));
|
||||
if (timeDiff==0) timeDiff = 1; // On older versions of the Recorder Bangle.js app the time reporting could be the same for two data points due to rounding.
|
||||
double calculation =
|
||||
analyticsObject.getJSONArray("Intermediate Distance").getDouble(i) / timeDiff;
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
//LOG.debug("check " + calculationsArray);
|
||||
analyticsObject.put("Speed", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
|
||||
//LOG.debug("check here 5");
|
||||
// Add average pace between last and current reading (s/km). (Was gonna do this as min/km but summary seems to expect s/km).
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
String speed = analyticsObject.getJSONArray("Speed").getString(i);
|
||||
//LOG.debug("check: " + speed);
|
||||
if (i==0 || Objects.equals(speed, "0") || Objects.equals(speed, "0.0") || Objects.equals(speed, "")) {
|
||||
calculationsArray.put("");
|
||||
} else {
|
||||
double calculation = (1000.0) * 1/ analyticsObject.getJSONArray("Speed").getDouble(i);
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Pace", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
}
|
||||
|
||||
//LOG.debug("check here 6");
|
||||
if (storedLogObject.has("Steps")) {
|
||||
for (int i = 0; i < logLength; i++) {
|
||||
if (i==0 || Objects.equals(storedLogObject.getJSONArray("Steps").getString(i), "")) {
|
||||
calculationsArray.put(0);
|
||||
} else {
|
||||
// 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 .
|
||||
double timeDiff =
|
||||
(storedLogObject.getJSONArray("Time").getDouble(i) -
|
||||
storedLogObject.getJSONArray("Time").getDouble(i-1));
|
||||
if (timeDiff==0) timeDiff = 1;
|
||||
double calculation = 0.5 * 60 *
|
||||
(storedLogObject.getJSONArray("Steps").getDouble(i) / timeDiff);
|
||||
calculationsArray.put(calculation);
|
||||
}
|
||||
}
|
||||
analyticsObject.put("Cadence", calculationsArray);
|
||||
|
||||
calculationsArray = new JSONArray();
|
||||
}
|
||||
//LOG.debug("check here AnalyticsObject:\n" + analyticsObject.toString());
|
||||
|
||||
//LOG.debug("check here 7");
|
||||
BaseActivitySummary summary = null;
|
||||
|
||||
Date startTime = new Date(parseLong(storedLogArray2[1][0].split("\\.\\d")[0])*1000L);
|
||||
Date endTime = new Date(parseLong(storedLogArray2[storedLogArray2.length-1][0].split("\\.\\d")[0])*1000L);
|
||||
summary = new BaseActivitySummary();
|
||||
Date startTime = new Date(banglePoints.get(0).getTime());
|
||||
Date endTime = new Date(banglePoints.get(banglePoints.size() - 1).getTime());
|
||||
summary.setName(log);
|
||||
summary.setStartTime(startTime);
|
||||
summary.setEndTime(endTime);
|
||||
ActivitySummaryData summaryData = BangleJSWorkoutParser.dataFromPoints(banglePoints);
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
ActivityKind activityKind;
|
||||
if (analyticsObject.has("Speed")) {
|
||||
if ((float) 3 > averageOfJSONArray(analyticsObject.getJSONArray("Speed"))) {
|
||||
final JSONObject speedAvgObj = summaryData.optJSONObject(SPEED_AVG);
|
||||
if (speedAvgObj != null) {
|
||||
double speedAvg;
|
||||
try {
|
||||
speedAvg = speedAvgObj.getDouble("value");
|
||||
} catch (JSONException e) {
|
||||
LOG.error("Failed to get speed avg");
|
||||
speedAvg = -1;
|
||||
}
|
||||
if ((float) 3 > speedAvg) {
|
||||
activityKind = ActivityKind.WALKING;
|
||||
} else {
|
||||
activityKind = ActivityKind.RUNNING;
|
||||
@ -403,133 +194,6 @@ class BangleJSActivityTrack {
|
||||
summary.setActivityKind(activityKind.getCode()); // TODO: Make this depend on info from watch (currently this info isn't supplied in Bangle.js recorder logs).
|
||||
summary.setRawDetailsPath(String.valueOf(inputFile));
|
||||
|
||||
|
||||
// FIXME: Many summaryData entries are commented out below. They currently don't report feasible results. Logic and calculation inside this function needs to be fixed.
|
||||
JSONObject summaryData = new JSONObject();
|
||||
// put("Activity", Arrays.asList(
|
||||
// "distanceMeters", "steps", "activeSeconds", "caloriesBurnt", "totalStride",
|
||||
// "averageHR", "maxHR", "minHR", "averageStride", "maxStride", "minStride"
|
||||
// ));
|
||||
if (analyticsObject.has("Intermediate Distance")) summaryData =
|
||||
addSummaryData(summaryData, ActivitySummaryEntries.DISTANCE_METERS,
|
||||
(float) analyticsObject.getJSONArray("Total Distance").getDouble(logLength - 1),
|
||||
"m");
|
||||
if (storedLogObject.has("Steps"))
|
||||
summaryData = addSummaryData(summaryData, "steps", sumOfJSONArray(storedLogObject.getJSONArray("Steps")), "steps");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.ACTIVE_SECONDS,3,"mm"); // FIXME: Is this suppose to exclude the time of inactivity in a workout?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.CALORIES_BURNT,3,"mm"); // TODO: Should this be calculated on Gadgetbridge side or be reported by Bangle.js?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.STRIDE_TOTAL,3,"mm"); // FIXME: What is this?
|
||||
if (storedLogObject.has("Heartrate")) {
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.HR_AVG, averageOfJSONArray(storedLogObject.getJSONArray("Heartrate")), "bpm");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.HR_MAX, maxOfJSONArray(storedLogObject.getJSONArray("Heartrate")), "bpm");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.HR_MIN, minOfJSONArray(storedLogObject.getJSONArray("Heartrate")), "bpm");
|
||||
}
|
||||
if (analyticsObject.has("Stride")) {
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.STRIDE_AVG,
|
||||
(float) (analyticsObject.getJSONArray("Total Distance").getDouble(logLength - 1) /
|
||||
(0.5 * sumOfJSONArray(storedLogObject.getJSONArray("Steps")))),
|
||||
"m"); // FIXME: Is this meant to be stride length as I've assumed?
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.STRIDE_MAX, maxOfJSONArray(analyticsObject.getJSONArray("Stride")), "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.STRIDE_MIN, minOfJSONArray(analyticsObject.getJSONArray("Stride")), "m");
|
||||
}
|
||||
|
||||
// put("Speed", Arrays.asList(
|
||||
// "averageSpeed", "maxSpeed", "minSpeed", "averageKMPaceSeconds", "minPace",
|
||||
// "maxPace", "averageSpeed2", "averageCadence", "maxCadence", "minCadence"
|
||||
// ));
|
||||
try {
|
||||
if (analyticsObject.has("Speed")) {
|
||||
summaryData = addSummaryData(summaryData,ActivitySummaryEntries.SPEED_AVG, averageOfJSONArray(analyticsObject.getJSONArray("Speed")),"m/s"); // This seems to be calculated somewhere else automatically.
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.SPEED_MAX, maxOfJSONArray(analyticsObject.getJSONArray("Speed")), "m/s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.SPEED_MIN, minOfJSONArray(analyticsObject.getJSONArray("Speed")), "m/s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_AVG_SECONDS_KM, averageOfJSONArray(analyticsObject.getJSONArray("Pace")), "s/km"); // Is this also calculated automatically then?
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_AVG_SECONDS_KM,
|
||||
// (float) (1000.0 * analyticsObject.getJSONArray("Elapsed Time").getDouble(logLength-1) /
|
||||
// analyticsObject.getJSONArray("Total Distance").getDouble(logLength-1)),
|
||||
// "s/km"
|
||||
//);
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_MIN, maxOfJSONArray(analyticsObject.getJSONArray("Pace")), "s/km");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.PACE_MAX, minOfJSONArray(analyticsObject.getJSONArray("Pace")), "s/km");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.averageSpeed2,3,"mm");
|
||||
}
|
||||
if (analyticsObject.has("Cadence")) {
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.averageCadence, averageOfJSONArray(analyticsObject.getJSONArray("Cadence")), "cycles/min"); // Is this also calculated automatically then?
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.CADENCE_AVG,
|
||||
(float) 0.5 * 60 * sumOfJSONArray(storedLogObject.getJSONArray("Steps")) /
|
||||
(float) analyticsObject.getJSONArray("Elapsed Time").getDouble(logLength - 1),
|
||||
"cycles/min"
|
||||
);
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.CADENCE_MAX, maxOfJSONArray(analyticsObject.getJSONArray("Cadence")), "cycles/min");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.CADENCE_MIN, minOfJSONArray(analyticsObject.getJSONArray("Cadence")), "cycles/min");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error(e + ". (thrown when trying to add summary data");
|
||||
}
|
||||
// private JSONObject createActivitySummaryGroups(){
|
||||
// final Map<String, List<String>> groupDefinitions = new HashMap<String, List<String>>() {{
|
||||
// put("Strokes", Arrays.asList(
|
||||
// "averageStrokeDistance", "averageStrokesPerSecond", "strokes"
|
||||
// ));
|
||||
|
||||
// put("Swimming", Arrays.asList(
|
||||
// "swolfIndex", "swimStyle"
|
||||
// ));
|
||||
|
||||
// put("Elevation", Arrays.asList(
|
||||
// "ascentMeters", "descentMeters", "maxAltitude", "minAltitude", "averageAltitude",
|
||||
// "baseAltitude", "ascentSeconds", "descentSeconds", "flatSeconds", "ascentDistance",
|
||||
// "descentDistance", "flatDistance", "elevationGain", "elevationLoss"
|
||||
// ));
|
||||
//}
|
||||
if (storedLogObject.has("Altitude") || storedLogObject.has("Barometer Altitude")) {
|
||||
String altitudeToUseKey = null;
|
||||
if (storedLogObject.has("Altitude")) {
|
||||
altitudeToUseKey = "Altitude";
|
||||
} else if (storedLogObject.has("Barometer Altitude")) {
|
||||
altitudeToUseKey = "Barometer Altitude";
|
||||
}
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_METERS, 3, "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_DISTANCE, 3, "m");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_MAX, maxOfJSONArray(storedLogObject.getJSONArray(altitudeToUseKey)), "m");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_MIN, minOfJSONArray(storedLogObject.getJSONArray(altitudeToUseKey)), "m");
|
||||
summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_AVG, averageOfJSONArray(storedLogObject.getJSONArray(altitudeToUseKey)), "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ALTITUDE_BASE, 3, "m");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_SECONDS, 3, "s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.DESCENT_SECONDS, 3, "s");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.FLAT_SECONDS, 3, "s");
|
||||
//if (analyticsObject.has("Intermittent Distance")) {
|
||||
// summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ASCENT_DISTANCE, 3, "m");
|
||||
// summaryData = addSummaryData(summaryData, ActivitySummaryEntries.DESCENT_DISTANCE, 3, "m");
|
||||
// summaryData = addSummaryData(summaryData, ActivitySummaryEntries.FLAT_DISTANCE, 3, "m");
|
||||
//}
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ELEVATION_GAIN, 3, "mm");
|
||||
//summaryData = addSummaryData(summaryData, ActivitySummaryEntries.ELEVATION_LOGG, 3, "mm");
|
||||
}
|
||||
// put("HeartRateZones", Arrays.asList(
|
||||
// "hrZoneNa", "hrZoneWarmUp", "hrZoneFatBurn", "hrZoneAerobic", "hrZoneAnaerobic",
|
||||
// "hrZoneExtreme"
|
||||
// ));
|
||||
// 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+")?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_NA,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_WARM_UP,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_FAT_BURN,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_AEROBIC,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_ANAEROBIC,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.HR_ZONE_EXTREME,3,"mm");
|
||||
// put("TrainingEffect", Arrays.asList(
|
||||
// "aerobicTrainingEffect", "anaerobicTrainingEffect", "currentWorkoutLoad",
|
||||
// "maximumOxygenUptake"
|
||||
// ));
|
||||
|
||||
// put("Laps", Arrays.asList(
|
||||
// "averageLapPace", "laps"
|
||||
// ));
|
||||
// TODO: Does Bangle.js report laps in recorder logs?
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.LAP_PACE_AVERAGE,3,"mm");
|
||||
//summaryData = addSummaryData(summaryData,ActivitySummaryEntries.LAPS,3,"mm");
|
||||
// }};
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
|
||||
ActivityTrack track = new ActivityTrack(); // detailsParser.parse(buffer.toByteArray());
|
||||
track.startNewSegment();
|
||||
track.setBaseTime(startTime);
|
||||
@ -543,36 +207,10 @@ class BangleJSActivityTrack {
|
||||
} catch (Exception ex) {
|
||||
GB.toast(context, "Error setting user for activity track.", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
ActivityPoint point = new ActivityPoint();
|
||||
Date timeOfPoint = new Date();
|
||||
boolean hasGPXReading = false;
|
||||
boolean hasHRMReading = false;
|
||||
for (int i = 0; i < storedLogObject.getJSONArray("Time").length(); i++) {
|
||||
timeOfPoint.setTime(storedLogObject.getJSONArray("Time").getLong(i)*1000L);
|
||||
point.setTime((Date) timeOfPoint.clone());
|
||||
if (storedLogObject.has("Longitude")) {
|
||||
if (!Objects.equals(storedLogObject.getJSONArray("Longitude").getString(i), "")
|
||||
&& !Objects.equals(storedLogObject.getJSONArray("Latitude").getString(i), "")
|
||||
&& !Objects.equals(storedLogObject.getJSONArray("Altitude").getString(i), "")) {
|
||||
|
||||
point.setLocation(new GPSCoordinate(
|
||||
storedLogObject.getJSONArray("Longitude").getDouble(i),
|
||||
storedLogObject.getJSONArray("Latitude").getDouble(i),
|
||||
storedLogObject.getJSONArray("Altitude").getDouble(i)
|
||||
)
|
||||
);
|
||||
|
||||
if (!hasGPXReading) hasGPXReading = true;
|
||||
}
|
||||
}
|
||||
if (storedLogObject.has("Heartrate") && !Objects.equals(storedLogObject.getJSONArray("Heartrate").getString(i), "")) {
|
||||
point.setHeartRate(storedLogObject.getJSONArray("Heartrate").getInt(i));
|
||||
|
||||
if (!hasHRMReading) hasHRMReading = true;
|
||||
}
|
||||
track.addTrackPoint(point);
|
||||
LOG.debug("Activity Point:\n" + point.getHeartRate());
|
||||
point = new ActivityPoint();
|
||||
boolean hasGPXReading = summaryData.has(INTERNAL_HAS_GPS);
|
||||
boolean hasHRMReading = summaryData.has(HR_AVG);
|
||||
for (final BangleJSActivityPoint banglePoint : banglePoints) {
|
||||
track.addTrackPoint(banglePoint.toActivityPoint());
|
||||
}
|
||||
|
||||
ActivityTrackExporter exporter = new GPXExporter();
|
||||
@ -605,13 +243,7 @@ class BangleJSActivityTrack {
|
||||
if (hasGPXReading /*|| hasHRMReading*/) {
|
||||
try {
|
||||
exporter.performExport(track, targetFile);
|
||||
|
||||
try (DBHandler dbHandler = GBApplication.acquireDB()) {
|
||||
summary.setGpxTrack(targetFile.getAbsolutePath());
|
||||
//dbHandler.getDaoSession().getBaseActivitySummaryDao().update(summary);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Could not add gpx track to summary:" + e);
|
||||
}
|
||||
summary.setGpxTrack(targetFile.getAbsolutePath());
|
||||
} catch (ActivityTrackExporter.GPXTrackEmptyException ex) {
|
||||
GB.toast(context, "This activity does not contain GPX tracks.", Toast.LENGTH_LONG, GB.ERROR, ex);
|
||||
}
|
||||
@ -634,8 +266,6 @@ class BangleJSActivityTrack {
|
||||
|
||||
} catch (IOException e) {
|
||||
LOG.error("IOException when parsing fetched CSV: " + e);
|
||||
} catch (JSONException e) {
|
||||
LOG.error("JSONException when parsing fetched CSV: " + e);
|
||||
}
|
||||
|
||||
stopAndRestartTimeout(device,context);
|
||||
@ -793,7 +423,7 @@ class BangleJSActivityTrack {
|
||||
int month = date.get(Calendar.MONTH);
|
||||
int day = date.get(Calendar.DATE);
|
||||
|
||||
return String.format("%d%02d%02d", year, month, day);
|
||||
return String.format(Locale.ROOT, "%d%02d%02d", year, month, day);
|
||||
}
|
||||
|
||||
private static void writeToRecorderCSV(String lines, File dir, String filename) {
|
||||
@ -811,86 +441,4 @@ class BangleJSActivityTrack {
|
||||
LOG.error("Could not write to file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONObject addSummaryData(JSONObject summaryData, String key, float value, String unit) {
|
||||
if (value > 0) {
|
||||
try {
|
||||
JSONObject innerData = new JSONObject();
|
||||
innerData.put("value", value);
|
||||
innerData.put("unit", unit);
|
||||
summaryData.put(key, innerData);
|
||||
} catch (JSONException ignore) {
|
||||
}
|
||||
}
|
||||
return summaryData;
|
||||
}
|
||||
|
||||
// protected JSONObject addSummaryData(JSONObject summaryData, String key, String value) {
|
||||
// if (key != null && !key.equals("") && value != null && !value.equals("")) {
|
||||
// try {
|
||||
// JSONObject innerData = new JSONObject();
|
||||
// innerData.put("value", value);
|
||||
// innerData.put("unit", "string");
|
||||
// summaryData.put(key, innerData);
|
||||
// } catch (JSONException ignore) {
|
||||
// }
|
||||
// }
|
||||
// return summaryData;
|
||||
// }
|
||||
|
||||
private static String distanceFromCoordinatePairs(String latA, String lonA, String latB, String lonB) {
|
||||
// https://en.wikipedia.org/wiki/Geographic_coordinate_system#Length_of_a_degree
|
||||
//phi = latitude
|
||||
//lambda = longitude
|
||||
//length of 1 degree lat:
|
||||
//111132.92 - 559.82*cos(2*phi) + 1.175*cos(4*phi) - 0.0023*cos(6*phi)
|
||||
//length of 1 degree lon:
|
||||
//111412.84*cos(phi) - 93.5*cos(3*phi) + 0.118*cos(5*phi)
|
||||
double latADouble = Double.parseDouble(latA);
|
||||
double latBDouble = Double.parseDouble(latB);
|
||||
double lonADouble = Double.parseDouble(lonA);
|
||||
double lonBDouble = Double.parseDouble(lonB);
|
||||
|
||||
double lengthPerDegreeLat = 111132.92 - 559.82*cos(2*latADouble) + 1.175*cos(4*latADouble) - 0.0023*cos(6*latADouble);
|
||||
double lengthPerDegreeLon = 111412.84*cos(latADouble) - 93.5*cos(3*latADouble) + 0.118*cos(5*latADouble);
|
||||
|
||||
double latDist = (latBDouble-latADouble)*lengthPerDegreeLat;
|
||||
double lonDist = (lonBDouble-lonADouble)*lengthPerDegreeLon;
|
||||
|
||||
return String.valueOf(sqrt(latDist*latDist+lonDist*lonDist));
|
||||
}
|
||||
|
||||
private static float sumOfJSONArray(JSONArray a) throws JSONException {
|
||||
double sum = 0;
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) sum += a.getDouble(i);
|
||||
}
|
||||
return (float) sum;
|
||||
}
|
||||
|
||||
private static float averageOfJSONArray(JSONArray a) throws JSONException {
|
||||
JSONArray b = new JSONArray();
|
||||
// Disregard empty lines.
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) b.put(a.getString(i));
|
||||
}
|
||||
return sumOfJSONArray(b) / b.length();
|
||||
}
|
||||
|
||||
private static float minOfJSONArray(JSONArray a) throws JSONException {
|
||||
double min = 999999999;
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) min = Math.min(min, a.getDouble(i));
|
||||
}
|
||||
return (float) min;
|
||||
}
|
||||
|
||||
private static float maxOfJSONArray(JSONArray a) throws JSONException {
|
||||
double max = -999999999;
|
||||
for (int i=0; i<a.length(); i++) {
|
||||
if (!Objects.equals(a.getString(i), "")) max = Math.max(max, a.getDouble(i));
|
||||
}
|
||||
return (float) max;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,47 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
/**
|
||||
* A simple class to accumulate stats (min, max, count, avg).
|
||||
*/
|
||||
public class Accumulator {
|
||||
private double min = Double.MAX_VALUE;
|
||||
private double max = -Double.MAX_VALUE;
|
||||
private double sum = 0;
|
||||
private int count;
|
||||
|
||||
public void add(final double value) {
|
||||
sum += value;
|
||||
count++;
|
||||
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
}
|
||||
|
||||
public double getMin() {
|
||||
return min;
|
||||
}
|
||||
|
||||
public double getMax() {
|
||||
return max;
|
||||
}
|
||||
|
||||
public double getSum() {
|
||||
return sum;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public double getAverage() {
|
||||
if (count > 0) {
|
||||
return sum / count;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.banglejs;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
|
||||
public class BangleJSActivityPointTest {
|
||||
@Test
|
||||
public void testParseCsvLine5s() {
|
||||
testTemplate(
|
||||
"Time,Latitude,Longitude,Altitude,Heartrate,Confidence,Source,Steps,Battery Percentage,Battery Voltage,Charging",
|
||||
"1710610740,,,,92,50,,0,96,3.30644531249,false",
|
||||
new BangleJSActivityPoint(
|
||||
1710610740_000L,
|
||||
null,
|
||||
92,
|
||||
50,
|
||||
"",
|
||||
0,
|
||||
96,
|
||||
3.30644531249,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
GPSCoordinate.UNKNOWN_ALTITUDE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCsvLine5sWithLocation() {
|
||||
testTemplate(
|
||||
"Time,Latitude,Longitude,Altitude,Heartrate,Confidence,Source,Steps,Battery Percentage,Battery Voltage,Charging",
|
||||
"1710610740,-65.999000,10.12300,,92,50,,0,96,3.30644531249,false",
|
||||
new BangleJSActivityPoint(
|
||||
1710610740_000L,
|
||||
new GPSCoordinate(10.123d, -65.999d),
|
||||
92,
|
||||
50,
|
||||
"",
|
||||
0,
|
||||
96,
|
||||
3.30644531249,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
GPSCoordinate.UNKNOWN_ALTITUDE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCsvLine5sWithLocationAndAltitude() {
|
||||
testTemplate(
|
||||
"Time,Latitude,Longitude,Altitude,Heartrate,Confidence,Source,Steps,Battery Percentage,Battery Voltage,Charging",
|
||||
"1710610740,-65.999000,10.12300,55,92,50,,0,96,3.30644531249,false",
|
||||
new BangleJSActivityPoint(
|
||||
1710610740_000L,
|
||||
new GPSCoordinate(10.123d, -65.999d, 55d),
|
||||
92,
|
||||
50,
|
||||
"",
|
||||
0,
|
||||
96,
|
||||
3.30644531249,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
GPSCoordinate.UNKNOWN_ALTITUDE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCsvLine1s() {
|
||||
testTemplate(
|
||||
"Time,Battery Percentage,Battery Voltage,Charging,Steps,Barometer Temperature,Barometer Pressure,Barometer Altitude,Heartrate,Confidence,Source,Latitude,Longitude,Altitude",
|
||||
"1700265185.2,78,3.31787109374,false,0,33.39859771728,1012.66780596669,4.84829130165,95.7,0,,,,",
|
||||
new BangleJSActivityPoint(
|
||||
1700265185_200L,
|
||||
null,
|
||||
96,
|
||||
0,
|
||||
"",
|
||||
0,
|
||||
78,
|
||||
3.31787109374,
|
||||
false,
|
||||
33.39859771728,
|
||||
1012.66780596669,
|
||||
4.84829130165
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseCsvLineBthrm() {
|
||||
testTemplate(
|
||||
"Time,Heartrate,Confidence,Source,Latitude,Longitude,Altitude,Int Heartrate,Int Confidence,BT Heartrate,BT Battery,Energy expended,Contact,RR,Barometer Temperature,Barometer Pressure,Barometer Altitude,Steps,Battery Percentage,Battery Voltage,Charging",
|
||||
"1727544008.4,61,100,bthrm,,,,0,32,61,,,,1069,31.20888417561,994.92400814020,153.70596141680,0,88,3.32226562499,false",
|
||||
new BangleJSActivityPoint(
|
||||
1727544008_400L,
|
||||
null,
|
||||
61,
|
||||
100,
|
||||
"bthrm",
|
||||
0,
|
||||
88,
|
||||
3.32226562499,
|
||||
false,
|
||||
31.20888417561,
|
||||
994.92400814020,
|
||||
153.70596141680
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private void testTemplate(final String headerStr, final String csvLine, final BangleJSActivityPoint expected) {
|
||||
final List<String> header = Arrays.asList(headerStr.split(","));
|
||||
final BangleJSActivityPoint point = BangleJSActivityPoint.fromCsvLine(header, csvLine);
|
||||
assertPointEquals(expected, point);
|
||||
}
|
||||
|
||||
private void assertPointEquals(final BangleJSActivityPoint expected, final BangleJSActivityPoint actual) {
|
||||
assertEquals("Mismatch on Time", expected.getTime(), actual.getTime());
|
||||
assertEquals("Mismatch on Location", expected.getLocation(), actual.getLocation());
|
||||
assertEquals("Mismatch on HeartRate", expected.getHeartRate(), actual.getHeartRate());
|
||||
assertEquals("Mismatch on HrConfidence", expected.getHrConfidence(), actual.getHrConfidence());
|
||||
assertEquals("Mismatch on HrSource", expected.getHrSource(), actual.getHrSource());
|
||||
assertEquals("Mismatch on Steps", expected.getSteps(), actual.getSteps());
|
||||
assertEquals("Mismatch on BatteryPercentage", expected.getBatteryPercentage(), actual.getBatteryPercentage());
|
||||
assertEquals("Mismatch on BatteryVoltage", expected.getBatteryVoltage(), actual.getBatteryVoltage(), 0.000001d);
|
||||
assertEquals("Mismatch on Charging", expected.isCharging(), actual.isCharging());
|
||||
assertEquals("Mismatch on BarometerTemperature", expected.getBarometerTemperature(), actual.getBarometerTemperature(), 0.000001d);
|
||||
assertEquals("Mismatch on BarometerPressure", expected.getBarometerPressure(), actual.getBarometerPressure(), 0.000001d);
|
||||
assertEquals("Mismatch on BarometerAltitude", expected.getBarometerAltitude(), actual.getBarometerAltitude(), 0.000001d);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.banglejs;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
|
||||
|
||||
public class BangleJSWorkoutParserTest extends TestBase {
|
||||
@Test
|
||||
@Ignore("helper test for development, remove this while debugging")
|
||||
public void testLocal() {
|
||||
final File file = new File("/storage/downloads/recorder.log20240317a.csv");
|
||||
final List<BangleJSActivityPoint> pointsFromCsv = BangleJSActivityPoint.fromCsv(file);
|
||||
assert pointsFromCsv != null;
|
||||
final ActivitySummaryData summaryData = BangleJSWorkoutParser.dataFromPoints(pointsFromCsv);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user