mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Zepp OS: Parse workout pace, cadence, strength sets
This commit is contained in:
+79
@@ -0,0 +1,79 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
|
||||
public class ActivitySummaryTableBuilder {
|
||||
private final String group;
|
||||
private final String headerId;
|
||||
private final List<String> headerColumns;
|
||||
private final Map<String, List<ActivitySummaryValue>> rows = new LinkedHashMap<>();
|
||||
|
||||
public ActivitySummaryTableBuilder(final String group, final String headerId, final List<String> headerColumns) {
|
||||
this.group = group;
|
||||
this.headerId = headerId;
|
||||
this.headerColumns = headerColumns;
|
||||
}
|
||||
|
||||
public void addRow(final String rowId, final List<ActivitySummaryValue> row) {
|
||||
if (row.size() != headerColumns.size()) {
|
||||
throw new IllegalArgumentException("Invalid number of row columns " + row.size());
|
||||
}
|
||||
|
||||
rows.put(rowId, row);
|
||||
}
|
||||
|
||||
public void addToSummaryData(final ActivitySummaryData summaryData) {
|
||||
final int numColumns = headerColumns.size();
|
||||
final boolean[] anyNonNull = new boolean[numColumns];
|
||||
|
||||
for (final List<ActivitySummaryValue> row : rows.values()) {
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
anyNonNull[i] |= row.get(i) != null;
|
||||
}
|
||||
}
|
||||
|
||||
final List<ActivitySummaryValue> finalHeader = new ArrayList<>(numColumns);
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
if (anyNonNull[i]) {
|
||||
finalHeader.add(new ActivitySummaryValue(headerColumns.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
summaryData.add(
|
||||
headerId,
|
||||
new ActivitySummaryTableRowEntry(
|
||||
group,
|
||||
finalHeader,
|
||||
true,
|
||||
true
|
||||
)
|
||||
);
|
||||
|
||||
for (final Map.Entry<String, List<ActivitySummaryValue>> e : rows.entrySet()) {
|
||||
final String key = e.getKey();
|
||||
final List<ActivitySummaryValue> row = e.getValue();
|
||||
final List<ActivitySummaryValue> finalRow = new ArrayList<>(numColumns);
|
||||
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
if (anyNonNull[i]) {
|
||||
finalRow.add(row.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
summaryData.add(
|
||||
key,
|
||||
new ActivitySummaryTableRowEntry(
|
||||
group,
|
||||
finalRow,
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -47,7 +47,7 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
|
||||
return null;
|
||||
}
|
||||
summaryData = new ActivitySummaryData();
|
||||
parseBinaryData(summary, startTime);
|
||||
parseBinaryData(summary, startTime, forDetails);
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
return summary;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public class HuamiActivitySummaryParser implements ActivitySummaryParser {
|
||||
return new HuamiActivityDetailsParser(summary);
|
||||
}
|
||||
|
||||
protected void parseBinaryData(BaseActivitySummary summary, Date startTime) {
|
||||
protected void parseBinaryData(BaseActivitySummary summary, Date startTime, final boolean forDetails) {
|
||||
final byte[] rawSummaryData = summary.getRawSummaryData();
|
||||
if (rawSummaryData == null) {
|
||||
return;
|
||||
|
||||
+58
-1
@@ -23,22 +23,32 @@ import android.content.Context;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryTableBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.HuamiProtos;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsActivityDetailsParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsActivityType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
|
||||
public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsActivitySummaryParser.class);
|
||||
@@ -54,7 +64,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime) {
|
||||
protected void parseBinaryData(final BaseActivitySummary summary, final Date startTime, final boolean forDetails) {
|
||||
final byte[] rawData = summary.getRawSummaryData();
|
||||
if (rawData == null) {
|
||||
return;
|
||||
@@ -208,5 +218,52 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
summaryData.add(STROKE_DISTANCE_AVG, summaryProto.getSwimmingData().getAvgDps(), UNIT_CM);
|
||||
summaryData.add(SWOLF_INDEX, summaryProto.getSwimmingData().getSwolf(), UNIT_NONE);
|
||||
}
|
||||
|
||||
if (forDetails && !StringUtils.isBlank(summary.getRawDetailsPath())) {
|
||||
try {
|
||||
enrichWithDetails(summary);
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed enrich summary", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void enrichWithDetails(final BaseActivitySummary summary) throws IOException, GBException {
|
||||
final File inputFile = FileUtils.tryFixPath(new File(summary.getRawDetailsPath()));
|
||||
if (inputFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final byte[] detailsBytes;
|
||||
try (InputStream inputStream = new FileInputStream(inputFile)) {
|
||||
detailsBytes = FileUtils.readAll(inputStream, inputFile.length());
|
||||
}
|
||||
|
||||
final ZeppOsActivityDetailsParser detailsParser = new ZeppOsActivityDetailsParser(summary);
|
||||
final ZeppOsActivityTrack activityTrack = detailsParser.parse(detailsBytes);
|
||||
List<ZeppOsActivityTrack.StrengthSet> strengthSets = activityTrack.getStrengthSets();
|
||||
if (!strengthSets.isEmpty()) {
|
||||
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(SETS, "sets_header", Arrays.asList(
|
||||
"set",
|
||||
"workout_set_reps",
|
||||
"menuitem_weight"
|
||||
));
|
||||
|
||||
int i = 1;
|
||||
for (final ZeppOsActivityTrack.StrengthSet strengthSet : strengthSets) {
|
||||
tableBuilder.addRow(
|
||||
"set_" + i,
|
||||
Arrays.asList(
|
||||
new ActivitySummaryValue(i, UNIT_NONE),
|
||||
new ActivitySummaryValue(String.valueOf(strengthSet.getReps())),
|
||||
new ActivitySummaryValue(strengthSet.getWeightKg() >= 0 ? strengthSet.getWeightKg() : null, UNIT_KG)
|
||||
)
|
||||
);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
tableBuilder.addToSummaryData(summaryData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ public class FetchSportsSummaryOperation extends AbstractFetchOperation {
|
||||
summary.setStartTime(getLastStartTimestamp().getTime()); // due to a bug this has to be set
|
||||
summary.setRawSummaryData(buffer.toByteArray());
|
||||
try {
|
||||
summary = summaryParser.parseBinaryData(summary, true);
|
||||
summary = summaryParser.parseBinaryData(summary, false);
|
||||
} catch (final Exception e) {
|
||||
GB.toast(getContext(), "Failed to parse activity summary", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
return false;
|
||||
|
||||
+26
-9
@@ -29,12 +29,12 @@ import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
|
||||
|
||||
@@ -54,7 +54,7 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
private long latitude;
|
||||
private double altitude;
|
||||
|
||||
private final ActivityTrack activityTrack;
|
||||
private final ZeppOsActivityTrack activityTrack;
|
||||
private ActivityPoint lastActivityPoint;
|
||||
|
||||
public ZeppOsActivityDetailsParser(final BaseActivitySummary summary) {
|
||||
@@ -64,14 +64,14 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
this.latitude = summary.getBaseLatitude();
|
||||
this.altitude = summary.getBaseAltitude();
|
||||
|
||||
this.activityTrack = new ActivityTrack();
|
||||
this.activityTrack = new ZeppOsActivityTrack();
|
||||
this.activityTrack.setUser(summary.getUser());
|
||||
this.activityTrack.setDevice(summary.getDevice());
|
||||
this.activityTrack.setName(createActivityName(summary));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActivityTrack parse(final byte[] bytes) throws GBException {
|
||||
public ZeppOsActivityTrack parse(final byte[] bytes) throws GBException {
|
||||
final ByteBuffer buf = ByteBuffer.wrap(bytes)
|
||||
.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
@@ -100,7 +100,7 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
unknownTypeCodes.put(typeCode, 0);
|
||||
}
|
||||
|
||||
unknownTypeCodes.put(typeCode, unknownTypeCodes.get(typeCode) + 1);
|
||||
unknownTypeCodes.put(typeCode, Objects.requireNonNull(unknownTypeCodes.get(typeCode)) + 1);
|
||||
//LOG.warn("Unknown type code {} of length {}", String.format("0x%X", typeCode), length);
|
||||
// Consume the reported length
|
||||
buf.get(new byte[length]);
|
||||
@@ -136,7 +136,8 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
consumeHeartRate(buf);
|
||||
break;
|
||||
case STRENGTH_SET:
|
||||
// TODO parse strength sets: weight, count, type
|
||||
consumeStrengthSet(buf);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("No consumer for for type {}", type);
|
||||
// Consume the reported length
|
||||
@@ -233,8 +234,6 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
LOG.warn("Unknown status code {}", String.format("0x%X", statusCode));
|
||||
}
|
||||
|
||||
// TODO split track into multiple segments?
|
||||
|
||||
//trace("Consumed Status: {}", status);
|
||||
}
|
||||
|
||||
@@ -245,7 +244,13 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
final short stride = buf.getShort(); // cm
|
||||
final short pace = buf.getShort(); // sec/km
|
||||
|
||||
// TODO integrate into gpx
|
||||
final ActivityPoint ap = getCurrentActivityPoint();
|
||||
if (ap != null) {
|
||||
ap.setCadence(cadence);
|
||||
if (pace != 0) {
|
||||
ap.setSpeed(1000f / pace); // s/km -> m/s
|
||||
}
|
||||
}
|
||||
|
||||
//trace("Consumed speed: cadence={}, stride={}, pace={}", cadence, stride, pace);
|
||||
}
|
||||
@@ -280,6 +285,18 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
//trace("Consumed HeartRate: {}", heartRate);
|
||||
}
|
||||
|
||||
private void consumeStrengthSet(final ByteBuffer buf) {
|
||||
buf.get(new byte[15]); // ?
|
||||
final int reps = buf.getShort() & 0xffff;
|
||||
buf.get(); // 0?
|
||||
final int weight = buf.getShort() & 0xffff;
|
||||
buf.get(new byte[14]); // ffff... ?
|
||||
|
||||
activityTrack.addStrengthSet(reps, weight != 0xffff ? weight / 10f : -1);
|
||||
|
||||
//trace("Consumed strength set: reps={}, weightKg={}", reps, weightKg);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ActivityPoint getCurrentActivityPoint() {
|
||||
if (lastActivityPoint == null) {
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
|
||||
public class ZeppOsActivityTrack extends ActivityTrack {
|
||||
public final List<StrengthSet> strengthSets = new ArrayList<>();
|
||||
|
||||
public void addStrengthSet(final int reps, final float weightKg) {
|
||||
strengthSets.add(new StrengthSet(reps, weightKg));
|
||||
}
|
||||
|
||||
public List<StrengthSet> getStrengthSets() {
|
||||
return strengthSets;
|
||||
}
|
||||
|
||||
public static class StrengthSet {
|
||||
private final int reps;
|
||||
private final float weightKg;
|
||||
|
||||
public StrengthSet(final int reps, final float weightKg) {
|
||||
this.reps = reps;
|
||||
this.weightKg = weightKg;
|
||||
}
|
||||
|
||||
public int getReps() {
|
||||
return reps;
|
||||
}
|
||||
|
||||
public float getWeightKg() {
|
||||
return weightKg;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user