1
0
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:
José Rebelo 2024-09-28 19:26:25 +01:00 committed by José Rebelo
parent dc8d295d4a
commit 5730e82380
8 changed files with 672 additions and 488 deletions

View File

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

View File

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

View File

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

View File

@ -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();

View File

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

View File

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

View File

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

View File

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