mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-06-02 03:16:07 +02:00
3a58314db6
- communication protocols - device support implementation - download FIT file storage Features: - basic connectivity: time sync, battery status, HW/FW version info - real-time activity tracking - fitness data sync - find the device, find the phone - factory reset Features implemented but not working: - notifications: fully implemented, seem to communicate correctly, but not shown on watch Features implemented partially (not expected to work now): - weather information (and in future possibly weather alerts) - music info - firmware update: only the initial file upload implemented, not used Things to improve/change: - Device name hardcoded in `VivomoveHrCoordinator.getSupportedType`, service UUIDs not available - Download FIT file storage: Should be store (and offer the user to export?) the FIT data forever? - Obviously, various code improvements, cleanup, etc.
273 lines
12 KiB
Java
273 lines
12 KiB
Java
package nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.fit;
|
|
|
|
import android.util.SparseIntArray;
|
|
import nodomain.freeyourgadget.gadgetbridge.devices.vivomovehr.VivomoveHrSampleProvider;
|
|
import nodomain.freeyourgadget.gadgetbridge.entities.VivomoveHrActivitySample;
|
|
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.GarminTimeUtils;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.SortedMap;
|
|
import java.util.TreeMap;
|
|
|
|
public class FitImporter {
|
|
private static final int ACTIVITY_TYPE_ALL = -1;
|
|
private final SortedMap<Integer, List<FitEvent>> eventsPerTimestamp = new TreeMap<>();
|
|
|
|
public void importFitData(List<FitMessage> messages) {
|
|
boolean ohrEnabled = false;
|
|
int softwareVersion = -1;
|
|
|
|
int lastTimestamp = 0;
|
|
final SparseIntArray lastCycles = new SparseIntArray();
|
|
|
|
for (FitMessage message : messages) {
|
|
switch (message.definition.globalMessageID) {
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_EVENT:
|
|
//message.getField();
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_SOFTWARE:
|
|
final Integer versionField = message.getIntegerField("version");
|
|
if (versionField != null) softwareVersion = versionField;
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MONITORING_INFO:
|
|
lastTimestamp = message.getIntegerField("timestamp");
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MONITORING:
|
|
lastTimestamp = processMonitoringMessage(message, ohrEnabled, lastTimestamp, lastCycles);
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_OHR_SETTINGS:
|
|
final Boolean isOhrEnabled = message.getBooleanField("enabled");
|
|
if (isOhrEnabled != null) ohrEnabled = isOhrEnabled;
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_SLEEP_LEVEL:
|
|
processSleepLevelMessage(message);
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MONITORING_HR_DATA:
|
|
processHrDataMessage(message);
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_STRESS_LEVEL:
|
|
processStressLevelMessage(message);
|
|
break;
|
|
|
|
case FitMessageDefinitions.FIT_MESSAGE_NUMBER_MAX_MET_DATA:
|
|
processMaxMetDataMessage(message);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void processImportedData(FitImportProcessor processor) {
|
|
for (final Map.Entry<Integer, List<FitEvent>> eventsForTimestamp : eventsPerTimestamp.entrySet()) {
|
|
final VivomoveHrActivitySample sample = new VivomoveHrActivitySample();
|
|
sample.setTimestamp(eventsForTimestamp.getKey());
|
|
|
|
sample.setRawKind(ActivitySample.NOT_MEASURED);
|
|
sample.setCaloriesBurnt(ActivitySample.NOT_MEASURED);
|
|
sample.setSteps(ActivitySample.NOT_MEASURED);
|
|
sample.setHeartRate(ActivitySample.NOT_MEASURED);
|
|
sample.setFloorsClimbed(ActivitySample.NOT_MEASURED);
|
|
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
|
|
|
|
FitEvent.EventKind bestKind = FitEvent.EventKind.UNKNOWN;
|
|
float bestScore = Float.NEGATIVE_INFINITY;
|
|
for (final FitEvent event : eventsForTimestamp.getValue()) {
|
|
if (event.getHeartRate() > sample.getHeartRate()) {
|
|
sample.setHeartRate(event.getHeartRate());
|
|
}
|
|
if (event.getFloorsClimbed() > sample.getFloorsClimbed()) {
|
|
sample.setFloorsClimbed(event.getFloorsClimbed());
|
|
}
|
|
|
|
float score = 0;
|
|
if (event.getRawKind() > 0) score += 1;
|
|
if (event.getCaloriesBurnt() > 0) score += event.getCaloriesBurnt() * 10.0f;
|
|
if (event.getSteps() > 0) score += event.getSteps();
|
|
//if (event.getRawIntensity() > 0) score += 10.0f * event.getRawIntensity();
|
|
if (event.getKind().isBetterThan(bestKind) || (event.getKind() == bestKind && score > bestScore)) {
|
|
// if (bestScore > Float.NEGATIVE_INFINITY && event.getKind() != FitEvent.EventKind.NOT_WORN) {
|
|
// System.out.println(String.format(Locale.ROOT, "Replacing %s %d (%d cal, %d steps) with %s %d (%d cal, %d steps)", sample.getRawKind(), sample.getRawIntensity(), sample.getCaloriesBurnt(), sample.getSteps(), event.getRawKind(), event.getRawIntensity(), event.getCaloriesBurnt(), event.getSteps()));
|
|
// }
|
|
bestScore = score;
|
|
bestKind = event.getKind();
|
|
sample.setRawKind(event.getRawKind());
|
|
sample.setCaloriesBurnt(event.getCaloriesBurnt());
|
|
sample.setSteps(event.getSteps());
|
|
sample.setRawIntensity(event.getRawIntensity());
|
|
}
|
|
}
|
|
|
|
if (sample.getHeartRate() == ActivitySample.NOT_MEASURED && ((sample.getRawKind() & VivomoveHrSampleProvider.RAW_TYPE_KIND_SLEEP) != 0)) {
|
|
sample.setRawKind(VivomoveHrSampleProvider.RAW_NOT_WORN);
|
|
sample.setRawIntensity(0);
|
|
}
|
|
|
|
processor.onSample(sample);
|
|
}
|
|
}
|
|
|
|
private void processSleepLevelMessage(FitMessage message) {
|
|
final Integer timestampFull = message.getIntegerField("timestamp");
|
|
final Integer sleepLevel = message.getIntegerField("sleep_level");
|
|
|
|
final int timestamp = GarminTimeUtils.garminTimestampToUnixTime(timestampFull);
|
|
final int rawIntensity = (4 - sleepLevel) * 40;
|
|
final int rawKind = VivomoveHrSampleProvider.RAW_TYPE_KIND_SLEEP | sleepLevel;
|
|
|
|
addEvent(new FitEvent(timestamp, FitEvent.EventKind.SLEEP, rawKind, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, rawIntensity));
|
|
}
|
|
|
|
private int processMonitoringMessage(FitMessage message, boolean ohrEnabled, int lastTimestamp, SparseIntArray lastCycles) {
|
|
final Integer activityType = message.getIntegerField("activity_type");
|
|
final Double activeCalories = message.getNumericField("active_calories");
|
|
final Integer intensity = message.getIntegerField("current_activity_type_intensity");
|
|
final Integer cycles = message.getIntegerField("cycles");
|
|
final Double heartRateMeasured = message.getNumericField("heart_rate");
|
|
final Integer timestampFull = message.getIntegerField("timestamp");
|
|
final Integer timestamp16 = message.getIntegerField("timestamp_16");
|
|
final Double activeTime = message.getNumericField("active_time");
|
|
|
|
final int activityTypeOrAll = activityType == null ? ACTIVITY_TYPE_ALL : activityType;
|
|
final int activityTypeOrDefault = activityType == null ? 0 : activityType;
|
|
|
|
final int lastDefaultCycleCount = lastCycles.get(ACTIVITY_TYPE_ALL);
|
|
final int lastCycleCount = Math.max(lastCycles.get(activityTypeOrAll), lastDefaultCycleCount);
|
|
final Integer currentCycles = cycles == null ? null : cycles < lastCycleCount ? cycles : cycles - lastCycleCount;
|
|
if (currentCycles != null) {
|
|
lastCycles.put(activityTypeOrDefault, cycles);
|
|
final int newAllCycles = Math.max(lastDefaultCycleCount, cycles);
|
|
if (newAllCycles != lastDefaultCycleCount) {
|
|
assert newAllCycles > lastDefaultCycleCount;
|
|
lastCycles.put(ACTIVITY_TYPE_ALL, newAllCycles);
|
|
}
|
|
}
|
|
|
|
if (timestampFull != null) {
|
|
lastTimestamp = timestampFull;
|
|
} else if (timestamp16 != null) {
|
|
lastTimestamp += (timestamp16 - (lastTimestamp & 0xFFFF)) & 0xFFFF;
|
|
} else {
|
|
// TODO: timestamp_min_8
|
|
throw new IllegalArgumentException("Unsupported timestamp");
|
|
}
|
|
|
|
final int timestamp = GarminTimeUtils.garminTimestampToUnixTime(lastTimestamp);
|
|
final int rawKind, caloriesBurnt, floorsClimbed, heartRate, steps, rawIntensity;
|
|
final FitEvent.EventKind eventKind;
|
|
|
|
caloriesBurnt = activeCalories == null ? ActivitySample.NOT_MEASURED : (int) Math.round(activeCalories);
|
|
floorsClimbed = ActivitySample.NOT_MEASURED;
|
|
heartRate = ohrEnabled && heartRateMeasured != null && heartRateMeasured > 0 ? (int) Math.round(heartRateMeasured) : ActivitySample.NOT_MEASURED;
|
|
steps = currentCycles == null ? ActivitySample.NOT_MEASURED : currentCycles;
|
|
rawIntensity = intensity == null ? 0 : intensity;
|
|
rawKind = VivomoveHrSampleProvider.RAW_TYPE_KIND_ACTIVITY | activityTypeOrDefault;
|
|
eventKind = steps != ActivitySample.NOT_MEASURED || rawIntensity > 0 || activityTypeOrDefault > 0 ? FitEvent.EventKind.ACTIVITY : FitEvent.EventKind.WORN;
|
|
|
|
if (rawKind != ActivitySample.NOT_MEASURED
|
|
|| caloriesBurnt != ActivitySample.NOT_MEASURED
|
|
|| floorsClimbed != ActivitySample.NOT_MEASURED
|
|
|| heartRate != ActivitySample.NOT_MEASURED
|
|
|| steps != ActivitySample.NOT_MEASURED
|
|
|| rawIntensity != ActivitySample.NOT_MEASURED) {
|
|
|
|
addEvent(new FitEvent(timestamp, eventKind, rawKind, caloriesBurnt, floorsClimbed, heartRate, steps, rawIntensity));
|
|
} else {
|
|
addEvent(new FitEvent(timestamp, FitEvent.EventKind.NOT_WORN, VivomoveHrSampleProvider.RAW_NOT_WORN, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED, ActivitySample.NOT_MEASURED));
|
|
}
|
|
return lastTimestamp;
|
|
}
|
|
|
|
private void processHrDataMessage(FitMessage message) {
|
|
}
|
|
|
|
private void processStressLevelMessage(FitMessage message) {
|
|
}
|
|
|
|
private void processMaxMetDataMessage(FitMessage message) {
|
|
}
|
|
|
|
private void addEvent(FitEvent event) {
|
|
List<FitEvent> eventsForTimestamp = eventsPerTimestamp.get(event.getTimestamp());
|
|
if (eventsForTimestamp == null) {
|
|
eventsForTimestamp = new ArrayList<>();
|
|
eventsPerTimestamp.put(event.getTimestamp(), eventsForTimestamp);
|
|
}
|
|
eventsForTimestamp.add(event);
|
|
}
|
|
|
|
private static class FitEvent {
|
|
private final int timestamp;
|
|
private final EventKind kind;
|
|
private final int rawKind;
|
|
private final int caloriesBurnt;
|
|
private final int floorsClimbed;
|
|
private final int heartRate;
|
|
private final int steps;
|
|
private final int rawIntensity;
|
|
|
|
private FitEvent(int timestamp, EventKind kind, int rawKind, int caloriesBurnt, int floorsClimbed, int heartRate, int steps, int rawIntensity) {
|
|
this.timestamp = timestamp;
|
|
this.kind = kind;
|
|
this.rawKind = rawKind;
|
|
this.caloriesBurnt = caloriesBurnt;
|
|
this.floorsClimbed = floorsClimbed;
|
|
this.heartRate = heartRate;
|
|
this.steps = steps;
|
|
this.rawIntensity = rawIntensity;
|
|
}
|
|
|
|
public int getTimestamp() {
|
|
return timestamp;
|
|
}
|
|
|
|
public EventKind getKind() {
|
|
return kind;
|
|
}
|
|
|
|
public int getRawKind() {
|
|
return rawKind;
|
|
}
|
|
|
|
public int getCaloriesBurnt() {
|
|
return caloriesBurnt;
|
|
}
|
|
|
|
public int getFloorsClimbed() {
|
|
return floorsClimbed;
|
|
}
|
|
|
|
public int getHeartRate() {
|
|
return heartRate;
|
|
}
|
|
|
|
public int getSteps() {
|
|
return steps;
|
|
}
|
|
|
|
public int getRawIntensity() {
|
|
return rawIntensity;
|
|
}
|
|
|
|
public enum EventKind {
|
|
UNKNOWN,
|
|
NOT_WORN,
|
|
WORN,
|
|
SLEEP,
|
|
ACTIVITY;
|
|
|
|
public boolean isBetterThan(EventKind other) {
|
|
return ordinal() > other.ordinal();
|
|
}
|
|
}
|
|
}
|
|
}
|