mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
CMF Watch Pro 2: Improve activity parsing (#4530)
This commit is contained in:
+1
-1
@@ -192,7 +192,7 @@ public class CmfWatchProCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
@Nullable
|
||||
@Override
|
||||
public ActivitySummaryParser getActivitySummaryParser(final GBDevice device, final Context context) {
|
||||
return new CmfWorkoutSummaryParser(device);
|
||||
return new CmfWorkoutSummaryParser(device, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+108
-13
@@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
/* Copyright (C) 2024-2025 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -17,12 +17,38 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.ACTIVE_SCORE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.CALORIES_BURNT;
|
||||
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_ZONE_AEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_ANAEROBIC;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_FAT_BURN;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_MAXIMUM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.HR_ZONE_WARM_UP;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.RECOVERY_TIME;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.STEPS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.TRAINING_LOAD;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_BPM;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_KCAL;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_METERS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_NONE;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_SECONDS;
|
||||
import static nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries.UNIT_STEPS;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryProgressEntry;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
@@ -31,10 +57,14 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.cmfwatchpro.CmfActivityType;
|
||||
|
||||
public class CmfWorkoutSummaryParser implements ActivitySummaryParser {
|
||||
private final GBDevice gbDevice;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CmfWorkoutSummaryParser.class);
|
||||
|
||||
public CmfWorkoutSummaryParser(final GBDevice device) {
|
||||
private final GBDevice gbDevice;
|
||||
private final Context context;
|
||||
|
||||
public CmfWorkoutSummaryParser(final GBDevice device, final Context context) {
|
||||
this.gbDevice = device;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -48,18 +78,14 @@ public class CmfWorkoutSummaryParser implements ActivitySummaryParser {
|
||||
|
||||
final ActivitySummaryData summaryData = new ActivitySummaryData();
|
||||
|
||||
// We should be able to get at least the first 3 right
|
||||
final int startTime = buf.getInt();
|
||||
final int duration = buf.getShort();
|
||||
final byte workoutType = buf.get();
|
||||
|
||||
buf.get(new byte[19]); // ?
|
||||
final int endTime = buf.getInt();
|
||||
final boolean gps = buf.get() == 1;
|
||||
buf.get(); // ?
|
||||
|
||||
summary.setStartTime(new Date(startTime * 1000L));
|
||||
summary.setEndTime(new Date(endTime * 1000L));
|
||||
|
||||
final int duration = buf.getShort();
|
||||
summaryData.add(ACTIVE_SECONDS, duration, UNIT_SECONDS);
|
||||
|
||||
final byte workoutType = buf.get();
|
||||
final CmfActivityType cmfActivityType = CmfActivityType.fromCode(workoutType);
|
||||
if (cmfActivityType != null) {
|
||||
summary.setActivityKind(cmfActivityType.getActivityKind().getCode());
|
||||
@@ -67,7 +93,76 @@ public class CmfWorkoutSummaryParser implements ActivitySummaryParser {
|
||||
summary.setActivityKind(ActivityKind.UNKNOWN.getCode());
|
||||
}
|
||||
|
||||
summaryData.add(ACTIVE_SECONDS, duration, UNIT_SECONDS);
|
||||
try {
|
||||
final int hrAvg = buf.get() & 0xff;
|
||||
summaryData.add(HR_AVG, hrAvg, UNIT_BPM);
|
||||
|
||||
final int calories = buf.getShort();
|
||||
summaryData.add(CALORIES_BURNT, calories, UNIT_KCAL);
|
||||
|
||||
final int steps = buf.getInt();
|
||||
summaryData.add(STEPS, steps, UNIT_STEPS);
|
||||
|
||||
final int distanceMeters = buf.getInt();
|
||||
summaryData.add(DISTANCE_METERS, distanceMeters, UNIT_METERS);
|
||||
|
||||
buf.get(new byte[8]); //?
|
||||
|
||||
final int endTime = buf.getInt();
|
||||
summary.setEndTime(new Date(endTime * 1000L));
|
||||
|
||||
final boolean gps = buf.get() == 1;
|
||||
buf.get(); // ?
|
||||
|
||||
if (buf.position() < buf.limit()) {
|
||||
// Watch 2 has more information
|
||||
final int trainingLoad = buf.getShort();
|
||||
summaryData.add(TRAINING_LOAD, trainingLoad, UNIT_NONE);
|
||||
|
||||
buf.get(new byte[4]); //?
|
||||
|
||||
final int recoveryTimeMinutes = buf.getShort();
|
||||
summaryData.add(RECOVERY_TIME, recoveryTimeMinutes * 60, UNIT_SECONDS);
|
||||
|
||||
final List<String> zones = Arrays.asList(HR_ZONE_WARM_UP, HR_ZONE_FAT_BURN, HR_ZONE_AEROBIC, HR_ZONE_ANAEROBIC, HR_ZONE_MAXIMUM);
|
||||
final int[] zoneTimes = new int[zones.size()];
|
||||
|
||||
zoneTimes[0] = buf.getShort() * 60;
|
||||
zoneTimes[1] = buf.getShort() * 60;
|
||||
zoneTimes[2] = buf.getShort() * 60;
|
||||
zoneTimes[3] = buf.getShort() * 60;
|
||||
zoneTimes[4] = buf.getShort() * 60;
|
||||
final float totalTimeInZone = Arrays.stream(zoneTimes).asLongStream().sum();
|
||||
if (totalTimeInZone > 0) {
|
||||
final int[] zoneColors = new int[]{
|
||||
context.getResources().getColor(R.color.hr_zone_warm_up_color),
|
||||
context.getResources().getColor(R.color.hr_zone_easy_color),
|
||||
context.getResources().getColor(R.color.hr_zone_aerobic_color),
|
||||
context.getResources().getColor(R.color.hr_zone_threshold_color),
|
||||
context.getResources().getColor(R.color.hr_zone_maximum_color),
|
||||
};
|
||||
|
||||
for (int i = 0; i < zones.size(); i++) {
|
||||
summaryData.add(
|
||||
zones.get(i),
|
||||
new ActivitySummaryProgressEntry(
|
||||
zoneTimes[i],
|
||||
UNIT_SECONDS,
|
||||
(int) ((100 * zoneTimes[i]) / totalTimeInZone),
|
||||
zoneColors[i]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final int activityScore = buf.getShort();
|
||||
summaryData.add(ACTIVE_SCORE, Math.round(activityScore / 1000f), UNIT_NONE);
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
LOG.error("Failed to parse workout binary data", e);
|
||||
}
|
||||
|
||||
summary.setSummaryData(summaryData.toString());
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
+1
@@ -93,6 +93,7 @@ public class ActivitySummaryEntries {
|
||||
public static final String AVG_TORQUE_EFFECTIVENESS = "average_torque_effectiveness";
|
||||
public static final String FRONT_GEAR_SHIFTS = "front_gear_shifts";
|
||||
public static final String REAR_GEAR_SHIFTS = "rear_gear_shifts";
|
||||
public static final String ACTIVE_SCORE = "workout_active_score";
|
||||
|
||||
public static final String AVG_VERTICAL_OSCILLATION = "vertical_oscillation";
|
||||
public static final String AVG_GROUND_CONTACT_TIME = "ground_contact_time";
|
||||
|
||||
+1
-1
@@ -397,7 +397,7 @@ public class CmfActivitySync {
|
||||
|
||||
final ByteBuffer buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
final CmfWorkoutSummaryParser summaryParser = new CmfWorkoutSummaryParser(getDevice());
|
||||
final CmfWorkoutSummaryParser summaryParser = new CmfWorkoutSummaryParser(getDevice(), getContext());
|
||||
|
||||
while (buf.remaining() > 0) {
|
||||
final byte[] summaryBytes = new byte[bytesPerWorkout];
|
||||
|
||||
@@ -2552,6 +2552,7 @@
|
||||
<string name="average_torque_effectiveness">Torque Effectiveness</string>
|
||||
<string name="front_gear_shifts">Front shifts</string>
|
||||
<string name="rear_gear_shifts">Rear shifts</string>
|
||||
<string name="workout_active_score">Active Score</string>
|
||||
<string name="vertical_oscillation">Vertical Oscillation</string>
|
||||
<string name="ground_contact_time">Ground Contact Time</string>
|
||||
<string name="vertical_ratio">Vertical Ratio</string>
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.test.TestBase;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
|
||||
public class CmfWorkoutSummaryParserTest extends TestBase {
|
||||
@Test
|
||||
public void testParseSummary2() {
|
||||
// From https://codeberg.org/Freeyourgadget/Gadgetbridge/issues/4530
|
||||
final byte[] bytes = GB.hexStringToByteArray("9F7734685D02039D8B0008060000FA0500007B01000000000000057A346800001B0064000000B40001000100000006000000401F0000");
|
||||
|
||||
final CmfWorkoutSummaryParser parser = new CmfWorkoutSummaryParser(null, getContext());
|
||||
final BaseActivitySummary summary = new BaseActivitySummary();
|
||||
summary.setRawSummaryData(bytes);
|
||||
parser.parseBinaryData(summary, false);
|
||||
|
||||
assertEquals(1748268959000L, summary.getStartTime().getTime());
|
||||
assertEquals(1748269573000L, summary.getEndTime().getTime());
|
||||
assertEquals(ActivityKind.INDOOR_RUNNING.getCode(), summary.getActivityKind());
|
||||
|
||||
final ActivitySummaryData summaryData = ActivitySummaryData.fromJson(summary.getSummaryData());
|
||||
assertNotNull(summaryData);
|
||||
|
||||
assertEquals(1530d, summaryData.getNumber(ActivitySummaryEntries.DISTANCE_METERS, -1));
|
||||
assertEquals(605d, summaryData.getNumber(ActivitySummaryEntries.ACTIVE_SECONDS, -1));
|
||||
assertEquals(139d, summaryData.getNumber(ActivitySummaryEntries.CALORIES_BURNT, -1));
|
||||
|
||||
// TODO max pace 6'23''
|
||||
// TODO avg speed 6.50kmph
|
||||
// TODO avg pace 6'19"
|
||||
|
||||
assertEquals(1544d, summaryData.getNumber(ActivitySummaryEntries.STEPS, -1));
|
||||
|
||||
// TODO avg step rate 151 steps/min
|
||||
// TODO avg step stride 102cm
|
||||
|
||||
assertEquals(157d, summaryData.getNumber(ActivitySummaryEntries.HR_AVG, -1));
|
||||
|
||||
// TODO max hr 190
|
||||
|
||||
assertEquals(1 * 60d, summaryData.getNumber(ActivitySummaryEntries.HR_ZONE_WARM_UP, -1)); // 1 min
|
||||
assertEquals(1 * 60d, summaryData.getNumber(ActivitySummaryEntries.HR_ZONE_FAT_BURN, -1)); // 1 min
|
||||
assertEquals(0d, summaryData.getNumber(ActivitySummaryEntries.HR_ZONE_AEROBIC, -1)); // 0 min
|
||||
assertEquals(6 * 60d, summaryData.getNumber(ActivitySummaryEntries.HR_ZONE_ANAEROBIC, -1)); // 6 min
|
||||
|
||||
// TODO aerobic endurance 1.0 / relaxed
|
||||
|
||||
assertEquals(27d, summaryData.getNumber(ActivitySummaryEntries.TRAINING_LOAD, -1));
|
||||
assertEquals(3 * 60 * 60d, summaryData.getNumber(ActivitySummaryEntries.RECOVERY_TIME, -1)); // 3h
|
||||
assertEquals(8d, summaryData.getNumber(ActivitySummaryEntries.ACTIVE_SCORE, -1)); // +8
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user