mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Zepp OS: Parse swimming details and temperature
This commit is contained in:
+51
-12
@@ -133,8 +133,13 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
}
|
||||
|
||||
if (summaryProto.hasPace()) {
|
||||
summaryData.add(PACE_MAX, summaryProto.getPace().getBest(), UNIT_SECONDS_PER_M);
|
||||
summaryData.add(PACE_AVG_SECONDS_KM, summaryProto.getPace().getAvg() * 1000, UNIT_SECONDS_PER_KM);
|
||||
if (ActivityKind.isSwimActivity(activityKind)) {
|
||||
summaryData.add(PACE_MAX, summaryProto.getPace().getBest() * 100, UNIT_SECONDS_PER_100_METERS);
|
||||
summaryData.add(PACE_AVG_SECONDS_KM, summaryProto.getPace().getAvg() * 100, UNIT_SECONDS_PER_100_METERS);
|
||||
} else {
|
||||
summaryData.add(PACE_MAX, summaryProto.getPace().getBest(), UNIT_SECONDS_PER_M);
|
||||
summaryData.add(PACE_AVG_SECONDS_KM, summaryProto.getPace().getAvg() * 1000, UNIT_SECONDS_PER_KM);
|
||||
}
|
||||
}
|
||||
|
||||
if (summaryProto.hasFrequency()) {
|
||||
@@ -204,6 +209,12 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
summaryData.add(DESCENT_SECONDS, summaryProto.getElevation().getDownhillTime(), UNIT_SECONDS);
|
||||
}
|
||||
|
||||
if (summaryProto.hasTemperature()) {
|
||||
summaryData.add(TEMPERATURE_MIN, summaryProto.getTemperature().getMin(), UNIT_CELSIUS);
|
||||
summaryData.add(TEMPERATURE_MAX, summaryProto.getTemperature().getMax(), UNIT_CELSIUS);
|
||||
summaryData.add(TEMPERATURE_AVG, summaryProto.getTemperature().getAvg(), UNIT_CELSIUS);
|
||||
}
|
||||
|
||||
if (summaryProto.hasSwimmingData()) {
|
||||
summaryData.add(LAPS, summaryProto.getSwimmingData().getLaps(), UNIT_LAPS);
|
||||
switch (summaryProto.getSwimmingData().getLaneLengthUnit()) {
|
||||
@@ -214,15 +225,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
summaryData.add(LANE_LENGTH, summaryProto.getSwimmingData().getLaneLength(), UNIT_YARD);
|
||||
break;
|
||||
}
|
||||
switch (summaryProto.getSwimmingData().getStyle()) {
|
||||
// TODO i18n these
|
||||
case 1:
|
||||
summaryData.add(SWIM_STYLE, "breaststroke");
|
||||
break;
|
||||
case 2:
|
||||
summaryData.add(SWIM_STYLE, "freestyle");
|
||||
break;
|
||||
}
|
||||
summaryData.add(SWIM_STYLE, getSwimStyle(summaryProto.getSwimmingData().getStyle()));
|
||||
summaryData.add(STROKES, summaryProto.getSwimmingData().getStrokes(), UNIT_STROKES);
|
||||
summaryData.add(STROKE_RATE_AVG, summaryProto.getSwimmingData().getAvgStrokeRate(), UNIT_STROKES_PER_MINUTE);
|
||||
summaryData.add(STROKE_RATE_MAX, summaryProto.getSwimmingData().getMaxStrokeRate(), UNIT_STROKES_PER_MINUTE);
|
||||
@@ -230,7 +233,7 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
summaryData.add(SWOLF_INDEX, summaryProto.getSwimmingData().getSwolf(), UNIT_NONE);
|
||||
}
|
||||
|
||||
if(summaryProto.hasMovementEvaluation()) {
|
||||
if (summaryProto.hasMovementEvaluation()) {
|
||||
summaryData.add(MOVEMENT_CONSISTENCY, summaryProto.getMovementEvaluation().getConsistency(), UNIT_NONE);
|
||||
summaryData.add(MOVEMENT_STABILITY, summaryProto.getMovementEvaluation().getStability(), UNIT_NONE);
|
||||
summaryData.add(MOVEMENT_CONTINUITY, summaryProto.getMovementEvaluation().getContinuity(), UNIT_NONE);
|
||||
@@ -293,5 +296,41 @@ public class ZeppOsActivitySummaryParser extends HuamiActivitySummaryParser {
|
||||
|
||||
tableBuilder.addToSummaryData(summaryData);
|
||||
}
|
||||
|
||||
final List<ZeppOsActivityTrack.SwimmingInterval> swimmingIntervals = zeppOsActivityTrack.getSwimmingIntervals();
|
||||
if (!swimmingIntervals.isEmpty()) {
|
||||
final ActivitySummaryTableBuilder tableBuilder = new ActivitySummaryTableBuilder(GROUP_INTERVALS, "intervals_header", Arrays.asList(
|
||||
"#",
|
||||
"swimming_stroke",
|
||||
"heart_rate",
|
||||
SWOLF_INDEX,
|
||||
"pref_header_time"
|
||||
));
|
||||
|
||||
for (final ZeppOsActivityTrack.SwimmingInterval interval : swimmingIntervals) {
|
||||
tableBuilder.addRow(
|
||||
"interval_" + interval.number(),
|
||||
Arrays.asList(
|
||||
new ActivitySummaryValue(interval.number(), UNIT_NONE),
|
||||
new ActivitySummaryValue(getSwimStyle(interval.style()), UNIT_NONE),
|
||||
new ActivitySummaryValue(interval.swolf(), UNIT_NONE),
|
||||
new ActivitySummaryValue(interval.hr(), UNIT_NONE),
|
||||
new ActivitySummaryValue(interval.durationMillis() / 1000, UNIT_SECONDS)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
tableBuilder.addToSummaryData(summaryData);
|
||||
}
|
||||
}
|
||||
|
||||
private String getSwimStyle(final int styleCode) {
|
||||
return switch (styleCode) {
|
||||
// TODO i18n these
|
||||
case 1 -> "breaststroke";
|
||||
case 2 -> "freestyle";
|
||||
case 3 -> "backstroke";
|
||||
default -> "unknown: " + styleCode;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ public class GPXExporter implements ActivityTrackExporter {
|
||||
XmlSerializer ser = Xml.newSerializer();
|
||||
try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
|
||||
ser.setOutput(outputStream, encoding);
|
||||
//ser.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
|
||||
ser.startDocument(encoding, Boolean.TRUE);
|
||||
ser.setPrefix("xsi", NS_XSI_URI);
|
||||
ser.setPrefix(NS_TRACKPOINT_EXTENSION, NS_TRACKPOINT_EXTENSION_URI);
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -40,6 +41,7 @@ public class ActivityPoint {
|
||||
private GPSCoordinate location;
|
||||
private int heartRate;
|
||||
private float speed = -1;
|
||||
private int strideCm = -1;
|
||||
private int cadence = -1;
|
||||
private int power = -1;
|
||||
private float respiratoryRate = -1;
|
||||
@@ -97,6 +99,14 @@ public class ActivityPoint {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public int getStrideCm() {
|
||||
return strideCm;
|
||||
}
|
||||
|
||||
public void setStrideCm(int strideCm) {
|
||||
this.strideCm = strideCm;
|
||||
}
|
||||
|
||||
public int getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
@@ -126,4 +136,154 @@ public class ActivityPoint {
|
||||
public double getDepth() {return depth; }
|
||||
public void setDepth(final double depth) { this.depth = depth; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (!(o instanceof ActivityPoint that)) return false;
|
||||
return heartRate == that.heartRate &&
|
||||
Float.compare(speed, that.speed) == 0 &&
|
||||
strideCm == that.strideCm &&
|
||||
cadence == that.cadence &&
|
||||
power == that.power &&
|
||||
Float.compare(respiratoryRate, that.respiratoryRate) == 0 &&
|
||||
Double.compare(depth, that.depth) == 0 &&
|
||||
Double.compare(temperature, that.temperature) == 0 &&
|
||||
Objects.equals(time, that.time) &&
|
||||
Objects.equals(location, that.location) &&
|
||||
Objects.equals(description, that.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(time, location, heartRate, speed, strideCm, cadence, power, respiratoryRate, depth, temperature, description);
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private long timeMillis;
|
||||
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private double altitude = GPSCoordinate.UNKNOWN_ALTITUDE;
|
||||
|
||||
private int heartRate;
|
||||
private float speed = -1;
|
||||
private int strideCm = -1;
|
||||
private int cadence = -1;
|
||||
private int power = -1;
|
||||
private float respiratoryRate = -1;
|
||||
private double depth = -1;
|
||||
private double temperature = -273;
|
||||
|
||||
public long getTime() {
|
||||
return timeMillis;
|
||||
}
|
||||
|
||||
public void setTime(final long timeMillis) {
|
||||
this.timeMillis = timeMillis;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public void setLatitude(final double latitude) {
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLongitude(final double longitude) {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public double getAltitude() {
|
||||
return altitude;
|
||||
}
|
||||
|
||||
public void setAltitude(final double altitude) {
|
||||
this.altitude = altitude;
|
||||
}
|
||||
|
||||
public int getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
public void setHeartRate(final int heartRate) {
|
||||
this.heartRate = heartRate;
|
||||
}
|
||||
|
||||
public float getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public void setSpeed(final float speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public int getStrideCm() {
|
||||
return strideCm;
|
||||
}
|
||||
|
||||
public void setStrideCm(int strideCm) {
|
||||
this.strideCm = strideCm;
|
||||
}
|
||||
|
||||
public int getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
public void setCadence(final int cadence) {
|
||||
this.cadence = cadence;
|
||||
}
|
||||
|
||||
public int getPower() {
|
||||
return power;
|
||||
}
|
||||
|
||||
public void setPower(final int power) {
|
||||
this.power = power;
|
||||
}
|
||||
|
||||
public float getRespiratoryRate() {
|
||||
return respiratoryRate;
|
||||
}
|
||||
|
||||
public void setRespiratoryRate(final float respiratoryRate) {
|
||||
this.respiratoryRate = respiratoryRate;
|
||||
}
|
||||
|
||||
public double getDepth() {
|
||||
return depth;
|
||||
}
|
||||
|
||||
public void setDepth(final double depth) {
|
||||
this.depth = depth;
|
||||
}
|
||||
|
||||
public double getTemperature() {
|
||||
return temperature;
|
||||
}
|
||||
|
||||
public void setTemperature(final double temperature) {
|
||||
this.temperature = temperature;
|
||||
}
|
||||
|
||||
public ActivityPoint build() {
|
||||
final ActivityPoint activityPoint = new ActivityPoint();
|
||||
activityPoint.setTime(new Date(timeMillis));
|
||||
if (latitude != 0 && longitude != 0) {
|
||||
activityPoint.setLocation(new GPSCoordinate(longitude, latitude, altitude));
|
||||
}
|
||||
activityPoint.setHeartRate(heartRate);
|
||||
activityPoint.setSpeed(speed);
|
||||
activityPoint.setCadence(cadence);
|
||||
activityPoint.setStrideCm(strideCm);
|
||||
activityPoint.setPower(power);
|
||||
activityPoint.setRespiratoryRate(respiratoryRate);
|
||||
activityPoint.setDepth(depth);
|
||||
activityPoint.setTemperature(temperature);
|
||||
return activityPoint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+148
-105
@@ -16,8 +16,6 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -27,6 +25,7 @@ import java.nio.ByteOrder;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -35,8 +34,8 @@ 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.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.AbstractHuamiActivityDetailsParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsActivityDetailsParser.class);
|
||||
@@ -47,23 +46,19 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
SDF.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private Date timestamp;
|
||||
private long offset = 0;
|
||||
|
||||
private long longitude;
|
||||
private long latitude;
|
||||
private double altitude;
|
||||
|
||||
private final ZeppOsActivityTrack activityTrack;
|
||||
private ActivityPoint lastActivityPoint;
|
||||
|
||||
// We need to keep track of these separately because of the offsets
|
||||
private long timestamp;
|
||||
private int offset;
|
||||
private int longitude;
|
||||
private int latitude;
|
||||
|
||||
private int lastSwimIntervalIndex = 0;
|
||||
|
||||
private final ActivityPoint.Builder activityPointBuilder = new ActivityPoint.Builder();
|
||||
|
||||
public ZeppOsActivityDetailsParser(final BaseActivitySummary summary) {
|
||||
this.timestamp = summary.getStartTime();
|
||||
|
||||
this.longitude = summary.getBaseLongitude();
|
||||
this.latitude = summary.getBaseLatitude();
|
||||
this.altitude = summary.getBaseAltitude();
|
||||
|
||||
this.activityTrack = new ZeppOsActivityTrack();
|
||||
this.activityTrack.setUser(summary.getUser());
|
||||
this.activityTrack.setDevice(summary.getDevice());
|
||||
@@ -131,16 +126,28 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
case HEARTRATE:
|
||||
consumeHeartRate(buf);
|
||||
break;
|
||||
case TEMPERATURE:
|
||||
consumeTemperature(buf);
|
||||
break;
|
||||
case STRENGTH_SET:
|
||||
consumeStrengthSet(buf);
|
||||
break;
|
||||
case SWIMMING_INTERVAL:
|
||||
consumeSwimmingInterval(buf);
|
||||
break;
|
||||
case LAP:
|
||||
consumeLap(buf);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("No consumer for for type {}", type);
|
||||
// Consume the reported length
|
||||
buf.get(new byte[length]);
|
||||
final byte[] unkBytes = new byte[length];
|
||||
buf.get(unkBytes);
|
||||
LOG.warn(
|
||||
"No consumer for for type {} at {}: {}",
|
||||
type,
|
||||
timestamp,
|
||||
GB.hexdump(unkBytes)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -153,7 +160,7 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
|
||||
if (!unknownTypeCodes.isEmpty()) {
|
||||
for (final Map.Entry<Integer, Integer> e : unknownTypeCodes.entrySet()) {
|
||||
LOG.warn("Unknown type code {} seen {} times", String.format("0x%X", e.getKey()), e.getValue());
|
||||
LOG.warn("Unknown type code {} seen {} times", e.getKey(), e.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,15 +217,21 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
}
|
||||
|
||||
private void consumeTimestamp(final ByteBuffer buf) {
|
||||
addNewActivityPoint();
|
||||
|
||||
buf.getInt(); // ?
|
||||
this.timestamp = new Date(buf.getLong());
|
||||
this.timestamp = buf.getLong();
|
||||
this.offset = 0;
|
||||
|
||||
trace("Consumed timestamp");
|
||||
trace("Consumed timestamp: {}", timestamp);
|
||||
}
|
||||
|
||||
private void consumeTimestampOffset(final ByteBuffer buf) {
|
||||
addNewActivityPoint();
|
||||
|
||||
this.offset = buf.getShort();
|
||||
|
||||
trace("Consumed offset: {}", offset);
|
||||
}
|
||||
|
||||
private void consumeGpsCoords(final ByteBuffer buf, final int length) {
|
||||
@@ -237,8 +250,6 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
|
||||
// TODO which one is the time offset? Not sure it is the first
|
||||
|
||||
addNewGpsCoordinates();
|
||||
|
||||
final double longitudeDeg = convertHuamiValueToDecimalDegrees(longitude);
|
||||
final double latitudeDeg = convertHuamiValueToDecimalDegrees(latitude);
|
||||
|
||||
@@ -251,23 +262,20 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
final short latitudeDelta = buf.getShort();
|
||||
final short two = buf.getShort(); // ? seems to always be 2
|
||||
|
||||
this.longitude += longitudeDelta;
|
||||
this.latitude += latitudeDelta;
|
||||
|
||||
// Handle additional data in Balance 2 format (16 bytes total)
|
||||
if (length == 16) {
|
||||
// Skip additional 8 bytes: 2-byte flag + 2x 4-byte floats (likely speed/accuracy)
|
||||
buf.get(new byte[8]);
|
||||
}
|
||||
|
||||
if (lastActivityPoint == null) {
|
||||
final String timestampStr = SDF.format(new Date(timestamp.getTime() + offset));
|
||||
if (this.longitude == 0 && this.latitude == 0) {
|
||||
final String timestampStr = SDF.format(new Date(timestamp + offset));
|
||||
LOG.warn("{}: Got GPS delta before GPS coords, ignoring", timestampStr);
|
||||
return;
|
||||
} else {
|
||||
this.longitude += longitudeDelta;
|
||||
this.latitude += latitudeDelta;
|
||||
}
|
||||
|
||||
addNewGpsCoordinates();
|
||||
|
||||
trace("Consumed GPS delta: {} {} {}", longitudeDelta, latitudeDelta, two);
|
||||
}
|
||||
|
||||
@@ -290,6 +298,7 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
break;
|
||||
case 6:
|
||||
status = "stop";
|
||||
addNewActivityPoint();
|
||||
break;
|
||||
default:
|
||||
status = String.format("unknown (0x%X)", statusCode);
|
||||
@@ -306,12 +315,10 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
final short stride = buf.getShort(); // cm
|
||||
final short pace = buf.getShort(); // sec/km
|
||||
|
||||
final ActivityPoint ap = getCurrentActivityPoint();
|
||||
if (ap != null) {
|
||||
ap.setCadence(cadence);
|
||||
if (pace != 0) {
|
||||
ap.setSpeed(1000f / pace); // s/km -> m/s
|
||||
}
|
||||
activityPointBuilder.setCadence(cadence);
|
||||
activityPointBuilder.setStrideCm(stride);
|
||||
if (pace != 0) {
|
||||
activityPointBuilder.setSpeed(1000f / pace); // s/km -> m/s
|
||||
}
|
||||
|
||||
trace("Consumed speed: cadence={}, stride={}, pace={}", cadence, stride, pace);
|
||||
@@ -341,35 +348,31 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
newAltitude = altitudeRaw / 100.0f;
|
||||
}
|
||||
|
||||
final ActivityPoint ap = getCurrentActivityPoint();
|
||||
if (ap != null) {
|
||||
final GPSCoordinate newCoordinate = new GPSCoordinate(
|
||||
ap.getLocation().getLongitude(),
|
||||
ap.getLocation().getLatitude(),
|
||||
newAltitude
|
||||
);
|
||||
activityPointBuilder.setAltitude(newAltitude);
|
||||
|
||||
ap.setLocation(newCoordinate);
|
||||
}
|
||||
|
||||
// Only update the instance altitude if we have valid data
|
||||
altitude = newAltitude;
|
||||
|
||||
trace("Consumed altitude: {}", altitude);
|
||||
trace("Consumed altitude: {}", newAltitude);
|
||||
}
|
||||
|
||||
private void consumeHeartRate(final ByteBuffer buf) {
|
||||
consumeTimestampOffset(buf);
|
||||
final int heartRate = buf.get() & 0xff;
|
||||
|
||||
final ActivityPoint ap = getCurrentActivityPoint();
|
||||
if (ap != null) {
|
||||
ap.setHeartRate(heartRate);
|
||||
}
|
||||
activityPointBuilder.setHeartRate(heartRate);
|
||||
|
||||
trace("Consumed HeartRate: {}", heartRate);
|
||||
}
|
||||
|
||||
private void consumeTemperature(final ByteBuffer buf) {
|
||||
consumeTimestampOffset(buf);
|
||||
|
||||
final float temperature = buf.getFloat();
|
||||
buf.get(); // 0?
|
||||
|
||||
activityPointBuilder.setTemperature(temperature);
|
||||
|
||||
trace("Consumed temperature: {}", temperature);
|
||||
}
|
||||
|
||||
private void consumeStrengthSet(final ByteBuffer buf) {
|
||||
buf.get(new byte[15]); // ?
|
||||
final int reps = buf.getShort() & 0xffff;
|
||||
@@ -382,6 +385,59 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
trace("Consumed strength set: reps={}, weightKg={}", reps, weight);
|
||||
}
|
||||
|
||||
private void consumeSwimmingInterval(final ByteBuffer buf) {
|
||||
consumeTimestampOffset(buf);
|
||||
|
||||
buf.get(); // 0?
|
||||
final int interval = buf.getShort(); // starting from 1
|
||||
final int poolLengthMeters = buf.getShort();
|
||||
buf.get(); // 0?
|
||||
final int hr = buf.get() & 0xff;
|
||||
buf.getShort(); // 1?
|
||||
final int style = buf.getShort() & 0xffff;
|
||||
final int pace = buf.getShort() & 0xffff; // s/km
|
||||
final int swolf = buf.getShort() & 0xffff;
|
||||
final int strokeRate = buf.getShort() & 0xffff;
|
||||
final int durationMillis = buf.getInt();
|
||||
buf.getInt(); // 0x00000000?
|
||||
final int strokeDistance = buf.getShort() & 0xffff;
|
||||
final int calories = buf.getShort() & 0xffff;
|
||||
|
||||
activityTrack.addSwimmingInterval(
|
||||
interval,
|
||||
poolLengthMeters,
|
||||
hr,
|
||||
style,
|
||||
pace,
|
||||
swolf,
|
||||
strokeRate,
|
||||
durationMillis,
|
||||
strokeDistance,
|
||||
calories
|
||||
);
|
||||
|
||||
final List<ActivityPoint> allPoints = activityTrack.getAllPoints();
|
||||
// Fill all activity points since the last interval so we can chart it
|
||||
for (int i = lastSwimIntervalIndex; i < allPoints.size(); i++) {
|
||||
allPoints.get(i).setSpeed(1000f / pace);
|
||||
allPoints.get(i).setCadence(strokeRate);
|
||||
}
|
||||
lastSwimIntervalIndex = allPoints.size();
|
||||
|
||||
trace(
|
||||
"Consumed swimming interval {}: hr={}, style={}, pace={}, swolf={}, strokeRate={}, durationMillis={}, strokeDistance={}, calories={}",
|
||||
interval,
|
||||
hr,
|
||||
style,
|
||||
pace,
|
||||
swolf,
|
||||
strokeRate,
|
||||
durationMillis,
|
||||
strokeDistance,
|
||||
calories
|
||||
);
|
||||
}
|
||||
|
||||
private void consumeLap(final ByteBuffer buf) {
|
||||
buf.get(new byte[2]); // ?
|
||||
final int number = buf.getShort() & 0xffff;
|
||||
@@ -399,59 +455,43 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
trace("Consumed lap: number={}, hr={}, pace={}, calories={}, distance={}, duration={}", number, hr, pace, calories, distance, duration);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ActivityPoint getCurrentActivityPoint() {
|
||||
if (lastActivityPoint == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Round to the nearest second
|
||||
final long currentTime = timestamp.getTime() + offset;
|
||||
if (currentTime - lastActivityPoint.getTime().getTime() > 500) {
|
||||
addNewGpsCoordinates();
|
||||
return lastActivityPoint;
|
||||
}
|
||||
|
||||
return lastActivityPoint;
|
||||
}
|
||||
|
||||
private void addNewGpsCoordinates() {
|
||||
final GPSCoordinate coordinate = new GPSCoordinate(
|
||||
convertHuamiValueToDecimalDegrees(longitude),
|
||||
convertHuamiValueToDecimalDegrees(latitude),
|
||||
altitude
|
||||
);
|
||||
|
||||
if (lastActivityPoint != null && lastActivityPoint.getLocation() != null && lastActivityPoint.getLocation().equals(coordinate)) {
|
||||
// Ignore repeated location
|
||||
private void addNewActivityPoint() {
|
||||
if (timestamp == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ActivityPoint ap = new ActivityPoint(new Date(timestamp.getTime() + offset));
|
||||
ap.setLocation(coordinate);
|
||||
add(ap);
|
||||
}
|
||||
|
||||
private void add(final ActivityPoint ap) {
|
||||
if (ap == lastActivityPoint) {
|
||||
LOG.debug("skipping point!");
|
||||
return;
|
||||
activityPointBuilder.setTime(Math.round((timestamp + offset) / 1000d) * 1000L);
|
||||
if (longitude != 0) {
|
||||
activityPointBuilder.setLongitude(convertHuamiValueToDecimalDegrees(longitude));
|
||||
}
|
||||
if (latitude != 0) {
|
||||
activityPointBuilder.setLatitude(convertHuamiValueToDecimalDegrees(latitude));
|
||||
}
|
||||
final ActivityPoint ap = activityPointBuilder.build();
|
||||
final List<ActivityPoint> currentSegment = activityTrack.getSegments().get(activityTrack.getSegments().size() - 1);
|
||||
// We get timestamp increments by milliseconds, and data interleaved with those
|
||||
// Upsert the last value for the same second if we got an update, as it will almost
|
||||
// surely contain updated information
|
||||
if (!currentSegment.isEmpty()) {
|
||||
if (currentSegment.get(currentSegment.size() - 1).getTime().equals(ap.getTime())) {
|
||||
trace("Upserting activity point at {}", ap.getTime());
|
||||
currentSegment.set(currentSegment.size() - 1, ap);
|
||||
} else {
|
||||
trace("Adding activity point at {}", ap.getTime());
|
||||
activityTrack.addTrackPoint(ap);
|
||||
}
|
||||
} else {
|
||||
trace("Adding activity point at {}", ap.getTime());
|
||||
activityTrack.addTrackPoint(ap);
|
||||
}
|
||||
|
||||
lastActivityPoint = ap;
|
||||
activityTrack.addTrackPoint(ap);
|
||||
}
|
||||
|
||||
private void trace(final String format, final Object... args) {
|
||||
final Object[] argsWithDate;
|
||||
if (timestamp != null) {
|
||||
argsWithDate = ArrayUtils.insert(0, args, SDF.format(new Date(timestamp.getTime() + offset)));
|
||||
} else {
|
||||
argsWithDate = ArrayUtils.insert(0, args, "(null)");
|
||||
}
|
||||
if (LOG.isTraceEnabled()) {
|
||||
final Object[] argsWithDate = ArrayUtils.insert(0, args, SDF.format(new Date(activityPointBuilder.getTime())));
|
||||
|
||||
//noinspection StringConcatenationArgumentToLogCall
|
||||
LOG.trace("{}: " + format, argsWithDate);
|
||||
//noinspection StringConcatenationArgumentToLogCall
|
||||
LOG.trace("{}: " + format, argsWithDate);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Type {
|
||||
@@ -463,18 +503,21 @@ public class ZeppOsActivityDetailsParser extends AbstractHuamiActivityDetailsPar
|
||||
ALTITUDE(7, 6),
|
||||
HEARTRATE(8, 3),
|
||||
LAP(11, 99),
|
||||
TEMPERATURE(13, 7),
|
||||
STRENGTH_SET(15, 34),
|
||||
SWIMMING_INTERVAL(20, 31),
|
||||
//UNKNOWN_7945(7945, 6),
|
||||
;
|
||||
|
||||
private final byte code;
|
||||
private final int code;
|
||||
private final int expectedLength;
|
||||
|
||||
Type(final int code, final int expectedLength) {
|
||||
this.code = (byte) code;
|
||||
this.code = code;
|
||||
this.expectedLength = expectedLength;
|
||||
}
|
||||
|
||||
public byte getCode() {
|
||||
public int getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
|
||||
+43
@@ -8,6 +8,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
|
||||
public class ZeppOsActivityTrack extends ActivityTrack {
|
||||
public final List<StrengthSet> strengthSets = new ArrayList<>();
|
||||
public final List<Lap> laps = new ArrayList<>();
|
||||
public final List<SwimmingInterval> swimmingIntervals = new ArrayList<>();
|
||||
|
||||
public void addStrengthSet(final int reps, final float weightKg) {
|
||||
strengthSets.add(new StrengthSet(reps, weightKg));
|
||||
@@ -22,6 +23,32 @@ public class ZeppOsActivityTrack extends ActivityTrack {
|
||||
laps.add(new Lap(number, hr, pace, calories, distance, duration));
|
||||
}
|
||||
|
||||
public void addSwimmingInterval(
|
||||
final int number,
|
||||
final int poolLengthMeters,
|
||||
final int hr,
|
||||
final int style,
|
||||
final int pace,
|
||||
final int swolf,
|
||||
final int strokeRate,
|
||||
final int durationMillis,
|
||||
final int strokeDistance,
|
||||
final int calories
|
||||
) {
|
||||
swimmingIntervals.add(new SwimmingInterval(
|
||||
number,
|
||||
poolLengthMeters,
|
||||
hr,
|
||||
style,
|
||||
pace,
|
||||
swolf,
|
||||
strokeRate,
|
||||
durationMillis,
|
||||
strokeDistance,
|
||||
calories
|
||||
));
|
||||
}
|
||||
|
||||
public List<StrengthSet> getStrengthSets() {
|
||||
return strengthSets;
|
||||
}
|
||||
@@ -30,6 +57,10 @@ public class ZeppOsActivityTrack extends ActivityTrack {
|
||||
return laps;
|
||||
}
|
||||
|
||||
public List<SwimmingInterval> getSwimmingIntervals() {
|
||||
return swimmingIntervals;
|
||||
}
|
||||
|
||||
public record StrengthSet(int reps, float weightKg) {
|
||||
}
|
||||
|
||||
@@ -40,4 +71,16 @@ public class ZeppOsActivityTrack extends ActivityTrack {
|
||||
int distance,
|
||||
int duration) {
|
||||
}
|
||||
|
||||
public record SwimmingInterval(int number,
|
||||
int poolLengthMeters,
|
||||
int hr,
|
||||
int style,
|
||||
int pace,
|
||||
int swolf,
|
||||
int strokeRate,
|
||||
int durationMillis,
|
||||
int strokeDistance,
|
||||
int calories) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ message WorkoutSummary {
|
||||
Pace pace = 10;
|
||||
Altitude altitude = 13;
|
||||
HeartRate heartRate = 19;
|
||||
Temperature temperature = 20;
|
||||
Calories calories = 16;
|
||||
Frequency frequency = 18;
|
||||
TrainingEffect trainingEffect = 21;
|
||||
@@ -41,6 +42,12 @@ message HeartRate {
|
||||
int32 min = 3; // bpm
|
||||
}
|
||||
|
||||
message Temperature {
|
||||
float avg = 1; // celsius
|
||||
float max = 2; // celsius
|
||||
float min = 3; // celsius
|
||||
}
|
||||
|
||||
message Steps {
|
||||
float avgCadence = 1; // steps/sec
|
||||
float maxCadence = 2; // steps/sec
|
||||
|
||||
Reference in New Issue
Block a user