CMF Watch Pro: Add ActivityTrackProvider

This commit is contained in:
José Rebelo
2026-03-21 21:55:04 +00:00
parent 32d5bbc1c6
commit de01e6ed14
4 changed files with 142 additions and 96 deletions
@@ -49,6 +49,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfActivityTrackProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfWorkoutSummaryParser;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
import nodomain.freeyourgadget.gadgetbridge.entities.CmfActivitySampleDao;
@@ -62,6 +63,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrackProvider;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
@@ -182,6 +184,12 @@ public class CmfWatchProCoordinator extends AbstractBLEDeviceCoordinator {
return new CmfWorkoutSummaryParser(device, context);
}
@Nullable
@Override
public ActivityTrackProvider getActivityTrackProvider(@NonNull final GBDevice device, @NonNull final Context context) {
return new CmfActivityTrackProvider(device);
}
@Override
public boolean supportsFlashing(@NonNull final GBDevice device) {
return true;
@@ -0,0 +1,130 @@
package nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfHeartRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfWorkoutGpsSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.CmfHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.CmfWorkoutGpsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityTrackProvider;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
@SuppressWarnings("ClassCanBeRecord")
public class CmfActivityTrackProvider implements ActivityTrackProvider {
private static final Logger LOG = LoggerFactory.getLogger(CmfActivityTrackProvider.class);
private final GBDevice gbDevice;
public CmfActivityTrackProvider(final GBDevice gbDevice) {
this.gbDevice = gbDevice;
}
@Nullable
@Override
public ActivityTrack getActivityTrack(@NonNull final BaseActivitySummary summary) {
final ActivityTrack track = new ActivityTrack();
track.setUser(summary.getUser());
track.setDevice(summary.getDevice());
track.setName(createActivityName(summary));
final List<CmfWorkoutGpsSample> gpsSamples;
final List<CmfHeartRateSample> hrSamples;
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final CmfWorkoutGpsSampleProvider gpsSampleProvider = new CmfWorkoutGpsSampleProvider(gbDevice, session);
gpsSamples = gpsSampleProvider.getAllSamples(summary.getStartTime().getTime(), summary.getEndTime().getTime());
final CmfHeartRateSampleProvider hrSampleProvider = new CmfHeartRateSampleProvider(gbDevice, session);
hrSamples = new ArrayList<>(hrSampleProvider.getAllSamples(summary.getStartTime().getTime(), summary.getEndTime().getTime()));
} catch (final Exception e) {
LOG.error("Error while building activity track", e);
return null;
}
Collections.sort(hrSamples, Comparator.comparingLong(CmfHeartRateSample::getTimestamp));
for (final CmfWorkoutGpsSample gpsSample : gpsSamples) {
final ActivityPoint ap = new ActivityPoint(new Date(gpsSample.getTimestamp()));
final GPSCoordinate coordinate = new GPSCoordinate(
gpsSample.getLongitude() / 10000000d,
gpsSample.getLatitude() / 10000000d
);
ap.setLocation(coordinate);
final CmfHeartRateSample hrSample = findNearestSample(hrSamples, gpsSample.getTimestamp());
if (hrSample != null) {
ap.setHeartRate(hrSample.getHeartRate());
}
track.addTrackPoint(ap);
}
return track;
}
@Nullable
private CmfHeartRateSample findNearestSample(final List<CmfHeartRateSample> samples, final long timestamp) {
if (samples.isEmpty()) {
return null;
}
if (timestamp < samples.get(0).getTimestamp()) {
return samples.get(0);
}
if (timestamp > samples.get(samples.size() - 1).getTimestamp()) {
return samples.get(samples.size() - 1);
}
int start = 0;
int end = samples.size() - 1;
while (start <= end) {
final int mid = (start + end) / 2;
if (timestamp < samples.get(mid).getTimestamp()) {
end = mid - 1;
} else if (timestamp > samples.get(mid).getTimestamp()) {
start = mid + 1;
} else {
return samples.get(mid);
}
}
// FIXME return null if too far?
if (samples.get(start).getTimestamp() - timestamp < timestamp - samples.get(end).getTimestamp()) {
return samples.get(start);
}
return samples.get(end);
}
protected static String createActivityName(final BaseActivitySummary summary) {
String name = summary.getName();
String nameText = "";
Long id = summary.getId();
if (name != null) {
nameText = name + " - ";
}
return nameText + id;
}
}
@@ -119,6 +119,7 @@ public class CmfWorkoutSummaryParser implements ActivitySummaryParser {
summary.setEndTime(new Date(endTime * 1000L));
final boolean gps = buf.get() == 1;
summaryData.setHasGps(gps);
buf.get(); // ?
if (buf.position() < buf.limit()) {
@@ -29,8 +29,6 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import de.greenrobot.dao.query.QueryBuilder;
@@ -45,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfSleep
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.samples.CmfWorkoutGpsSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfActivityTrackProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.cmfwatchpro.workout.CmfWorkoutSummaryParser;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
@@ -62,9 +61,7 @@ import nodomain.freeyourgadget.gadgetbridge.export.ActivityTrackExporter;
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.ActivityTrack;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -501,7 +498,8 @@ public class CmfActivitySync {
}
private void processGps(final BaseActivitySummary summary) {
final ActivityTrack activityTrack = buildActivityTrack(summary);
final CmfActivityTrackProvider activityTrackProvider = new CmfActivityTrackProvider(getDevice());
final ActivityTrack activityTrack = activityTrackProvider.getActivityTrack(summary);
if (activityTrack == null) {
return;
}
@@ -567,97 +565,6 @@ public class CmfActivitySync {
return gpxTargetFile;
}
@Nullable
private ActivityTrack buildActivityTrack(final BaseActivitySummary summary) {
final ActivityTrack track = new ActivityTrack();
track.setUser(summary.getUser());
track.setDevice(summary.getDevice());
track.setName(createActivityName(summary));
final List<CmfWorkoutGpsSample> gpsSamples;
final List<CmfHeartRateSample> hrSamples;
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final CmfWorkoutGpsSampleProvider gpsSampleProvider = new CmfWorkoutGpsSampleProvider(getDevice(), session);
gpsSamples = gpsSampleProvider.getAllSamples(summary.getStartTime().getTime(), summary.getEndTime().getTime());
final CmfHeartRateSampleProvider hrSampleProvider = new CmfHeartRateSampleProvider(getDevice(), session);
hrSamples = new ArrayList<>(hrSampleProvider.getAllSamples(summary.getStartTime().getTime(), summary.getEndTime().getTime()));
} catch (final Exception e) {
LOG.error("Error while building activity track", e);
return null;
}
Collections.sort(hrSamples, (a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp()));
for (final CmfWorkoutGpsSample gpsSample : gpsSamples) {
final ActivityPoint ap = new ActivityPoint(new Date(gpsSample.getTimestamp()));
final GPSCoordinate coordinate = new GPSCoordinate(
gpsSample.getLongitude() / 10000000d,
gpsSample.getLatitude() / 10000000d
);
ap.setLocation(coordinate);
final CmfHeartRateSample hrSample = findNearestSample(hrSamples, gpsSample.getTimestamp());
if (hrSample != null) {
ap.setHeartRate(hrSample.getHeartRate());
}
track.addTrackPoint(ap);
}
return track;
}
@Nullable
private CmfHeartRateSample findNearestSample(final List<CmfHeartRateSample> samples, final long timestamp) {
if (samples.isEmpty()) {
return null;
}
if (timestamp < samples.get(0).getTimestamp()) {
return samples.get(0);
}
if (timestamp > samples.get(samples.size() - 1).getTimestamp()) {
return samples.get(samples.size() - 1);
}
int start = 0;
int end = samples.size() - 1;
while (start <= end) {
final int mid = (start + end) / 2;
if (timestamp < samples.get(mid).getTimestamp()) {
end = mid - 1;
} else if (timestamp > samples.get(mid).getTimestamp()) {
start = mid + 1;
} else {
return samples.get(mid);
}
}
// FIXME return null if too far?
if (samples.get(start).getTimestamp() - timestamp < timestamp - samples.get(end).getTimestamp()) {
return samples.get(start);
}
return samples.get(end);
}
protected static String createActivityName(final BaseActivitySummary summary) {
String name = summary.getName();
String nameText = "";
Long id = summary.getId();
if (name != null) {
nameText = name + " - ";
}
return nameText + id;
}
private Context getContext() {
return mSupport.getContext();
}