1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-25 18:15:49 +01:00

Garmin: Parse and persist HRV_STATUS

This commit is contained in:
José Rebelo 2024-08-02 21:42:18 +01:00
parent 532c545093
commit b33ab0b1c8
20 changed files with 660 additions and 46 deletions

View File

@ -45,7 +45,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
final Schema schema = new Schema(74, MAIN_PACKAGE + ".entities");
final Schema schema = new Schema(75, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -113,6 +113,8 @@ public class GBDaoGenerator {
addGarminSpo2Sample(schema, user, device);
addGarminSleepStageSample(schema, user, device);
addGarminEventSample(schema, user, device);
addGarminHrvSummarySample(schema, user, device);
addGarminHrvValueSample(schema, user, device);
addWena3EnergySample(schema, user, device);
addWena3BehaviorSample(schema, user, device);
addWena3CaloriesSample(schema, user, device);
@ -725,6 +727,26 @@ public class GBDaoGenerator {
return sleepStageSample;
}
private static Entity addGarminHrvSummarySample(Schema schema, Entity user, Entity device) {
Entity hrvSummarySample = addEntity(schema, "GarminHrvSummarySample");
addCommonTimeSampleProperties("AbstractHrvSummarySample", hrvSummarySample, user, device);
hrvSummarySample.addIntProperty("weeklyAverage").codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty("lastNightAverage").codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty("lastNight5MinHigh").codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty("baselineLowUpper").codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty("baselineBalancedLower").codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty("baselineBalancedUpper").codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty("statusNum").codeBeforeGetter(OVERRIDE);
return hrvSummarySample;
}
private static Entity addGarminHrvValueSample(Schema schema, Entity user, Entity device) {
Entity hrvValueSample = addEntity(schema, "GarminHrvValueSample");
addCommonTimeSampleProperties("AbstractHrvValueSample", hrvValueSample, user, device);
hrvValueSample.addIntProperty("value").notNull().codeBeforeGetter(OVERRIDE);
return hrvValueSample;
}
private static Entity addWatchXPlusHealthActivitySample(Schema schema, Entity user, Entity device) {
Entity activitySample = addEntity(schema, "WatchXPlusActivitySample");
activitySample.implementsSerializable();

View File

@ -73,6 +73,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
@ -208,6 +210,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return null;
}
@Override
public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public int[] getStressRanges() {
// 0-39 = relaxed
@ -442,6 +454,11 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
return false;
}
@Override
public boolean supportsHrvMeasurement() {
return false;
}
@Override
public boolean supportsActivityTabs() {
return supportsActivityTracking();

View File

@ -52,6 +52,8 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
import nodomain.freeyourgadget.gadgetbridge.model.SleepRespiratoryRateSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
@ -214,6 +216,8 @@ public interface DeviceCoordinator {
*/
boolean supportsStressMeasurement();
boolean supportsHrvMeasurement();
boolean supportsSleepMeasurement();
boolean supportsStepCounter();
boolean supportsSpeedzones();
@ -283,6 +287,16 @@ public interface DeviceCoordinator {
*/
TimeSampleProvider<? extends StressSample> getStressSampleProvider(GBDevice device, DaoSession session);
/**
* Returns the sample provider for HRV summary, for the device being supported.
*/
TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(GBDevice device, DaoSession session);
/**
* Returns the sample provider for HRV values, for the device being supported.
*/
TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(GBDevice device, DaoSession session);
/**
* Returns the stress ranges (relaxed, mild, moderate, high), so that stress can be categorized.
*/

View File

@ -24,6 +24,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
@ -96,6 +98,16 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return new GarminStressSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(final GBDevice device, final DaoSession session) {
return new GarminHrvSummarySampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(final GBDevice device, final DaoSession session) {
return new GarminHrvValueSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(final GBDevice device, final DaoSession session) {
return new GarminSpo2SampleProvider(device, session);
@ -162,6 +174,11 @@ public abstract class GarminCoordinator extends AbstractBLEDeviceCoordinator {
return true;
}
@Override
public boolean supportsHrvMeasurement() {
return true;
}
@Override
public int[] getStressRanges() {
// 1-25 = relaxed

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GarminHrvSummarySampleProvider extends AbstractTimeSampleProvider<GarminHrvSummarySample> {
public GarminHrvSummarySampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<GarminHrvSummarySample, ?> getSampleDao() {
return getSession().getGarminHrvSummarySampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return GarminHrvSummarySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return GarminHrvSummarySampleDao.Properties.DeviceId;
}
@Override
public GarminHrvSummarySample createSample() {
return new GarminHrvSummarySample();
}
}

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.garmin;
import androidx.annotation.NonNull;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractTimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GarminHrvValueSampleProvider extends AbstractTimeSampleProvider<GarminHrvValueSample> {
public GarminHrvValueSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<GarminHrvValueSample, ?> getSampleDao() {
return getSession().getGarminHrvValueSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return GarminHrvValueSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return GarminHrvValueSampleDao.Properties.DeviceId;
}
@Override
public GarminHrvValueSample createSample() {
return new GarminHrvValueSample();
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.entities;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.model.HrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public abstract class AbstractHrvSummarySample extends AbstractTimeSample implements HrvSummarySample {
public Integer getStatusNum() {
return Status.NONE.getNum();
}
@Override
public Status getStatus() {
return Status.fromNum(getStatusNum());
}
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) +
", weeklyAverage=" + getWeeklyAverage() +
", lastNightAverage=" + getLastNightAverage() +
", lastNight5MinHigh=" + getLastNight5MinHigh() +
", baselineLowUpper=" + getBaselineLowUpper() +
", baselineBalancedLower=" + getBaselineBalancedLower() +
", baselineBalancedUpper=" + getBaselineBalancedUpper() +
", status=" + getStatus() +
", userId=" + getUserId() +
", deviceId=" + getDeviceId() +
"}";
}
}

View File

@ -0,0 +1,35 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.entities;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils;
public abstract class AbstractHrvValueSample extends AbstractTimeSample implements HrvValueSample {
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"timestamp=" + DateTimeUtils.formatDateTime(DateTimeUtils.parseTimestampMillis(getTimestamp())) +
", value=" + getValue() +
", userId=" + getUserId() +
", deviceId=" + getDeviceId() +
"}";
}
}

View File

@ -0,0 +1,80 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
public interface HrvSummarySample extends TimeSample {
enum Status {
NONE(0),
POOR(1),
LOW(2),
UNBALANCED(3),
BALANCED(4),
;
private final int num;
Status(final int num) {
this.num = num;
}
public int getNum() {
return num;
}
public static Status fromNum(final int num) {
for (Status value : Status.values()) {
if (value.getNum() == num) {
return value;
}
}
throw new IllegalArgumentException("Unknown hrv status num " + num);
}
}
/**
* Weekly average, in milliseconds.
*/
Integer getWeeklyAverage();
/**
* Last night average, in milliseconds.
*/
Integer getLastNightAverage();
/**
* Last night 5-min high, in milliseconds.
*/
Integer getLastNight5MinHigh();
/**
* Baseline low upper, in milliseconds.
*/
Integer getBaselineLowUpper();
/**
* Baseline balanced lower, in milliseconds.
*/
Integer getBaselineBalancedLower();
/**
* Baseline balanced upper, in milliseconds.
*/
Integer getBaselineBalancedUpper();
Status getStatus();
}

View File

@ -0,0 +1,24 @@
/* Copyright (C) 2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model;
public interface HrvValueSample extends TimeSample {
/**
* HRV value, in milliseconds.
*/
int getValue();
}

View File

@ -6,6 +6,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefi
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
@ -30,6 +31,8 @@ public class FieldDefinitionFactory {
return new FieldDefinitionGoalSource(localNumber, size, baseType, name);
case GOAL_TYPE:
return new FieldDefinitionGoalType(localNumber, size, baseType, name);
case HRV_STATUS:
return new FieldDefinitionHrvStatus(localNumber, size, baseType, name);
case MEASUREMENT_SYSTEM:
return new FieldDefinitionMeasurementSystem(localNumber, size, baseType, name);
case TEMPERATURE:
@ -55,6 +58,7 @@ public class FieldDefinitionFactory {
FILE_TYPE,
GOAL_SOURCE,
GOAL_TYPE,
HRV_STATUS,
MEASUREMENT_SYSTEM,
TEMPERATURE,
TIMESTAMP,

View File

@ -32,6 +32,8 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminEventSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStageSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminStressSampleProvider;
@ -40,6 +42,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSpo2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminStressSample;
@ -51,9 +55,12 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminTimeUtils;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.enums.GarminSport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitFileId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitHrvSummary;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitHrvValue;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoring;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitSession;
@ -75,6 +82,8 @@ public class FitImporter {
private final List<GarminSpo2Sample> spo2samples = new ArrayList<>();
private final List<GarminEventSample> events = new ArrayList<>();
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
private final List<GarminHrvSummarySample> hrvSummarySamples = new ArrayList<>();
private final List<GarminHrvValueSample> hrvValueSamples = new ArrayList<>();
private final List<FitTimeInZone> timesInZone = new ArrayList<>();
private final List<ActivityPoint> activityPoints = new ArrayList<>();
private final Map<Integer, Integer> unknownRecords = new HashMap<>();
@ -179,6 +188,34 @@ public class FitImporter {
} else if (record instanceof FitTimeInZone) {
LOG.trace("Time in zone: {}", record);
timesInZone.add((FitTimeInZone) record);
} else if (record instanceof FitHrvSummary) {
final FitHrvSummary hrvSummary = (FitHrvSummary) record;
final FieldDefinitionHrvStatus.HrvStatus status = hrvSummary.getStatus();
if (status == null) {
continue;
}
LOG.trace("HRV summary at {}: {}", ts, record);
final GarminHrvSummarySample sample = new GarminHrvSummarySample( );
sample.setTimestamp(ts * 1000L);
sample.setWeeklyAverage(hrvSummary.getWeeklyAverage());
sample.setLastNightAverage(hrvSummary.getLastNightAverage());
sample.setLastNight5MinHigh(hrvSummary.getLastNight5MinHigh());
sample.setBaselineLowUpper(hrvSummary.getBaselineLowUpper());
sample.setBaselineBalancedLower(hrvSummary.getBaselineBalancedLower());
sample.setBaselineBalancedUpper(hrvSummary.getBaselineBalancedUpper());
sample.setStatusNum(status.getId());
hrvSummarySamples.add(sample);
} else if (record instanceof FitHrvValue) {
final FitHrvValue hrvValue = (FitHrvValue) record;
if (hrvValue.getValue() == null) {
LOG.warn("HRV value at {} is null", ts);
continue;
}
LOG.trace("HRV value at {}: {}", ts, hrvValue.getValue());
final GarminHrvValueSample sample = new GarminHrvValueSample();
sample.setTimestamp(ts * 1000L);
sample.setValue(hrvValue.getValue());
hrvValueSamples.add(sample);
} else {
LOG.trace("Unknown record: {}", record);
@ -202,18 +239,22 @@ public class FitImporter {
}
switch (fileId.getType()) {
case activity:
case ACTIVITY:
persistWorkout(file);
break;
case monitor:
case MONITOR:
persistActivitySamples();
persistSpo2Samples();
persistStressSamples();
break;
case sleep:
case SLEEP:
persistEvents();
persistSleepStageSamples();
break;
case HRV_STATUS:
persistHrvSummarySamples();
persistHrvValueSamples();
break;
default:
LOG.warn("Unable to handle fit file of type {}", fileId.getType());
}
@ -512,6 +553,58 @@ public class FitImporter {
}
}
private void persistHrvSummarySamples() {
if (hrvSummarySamples.isEmpty()) {
return;
}
LOG.debug("Will persist {} HRV summary samples", hrvSummarySamples.size());
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
final GarminHrvSummarySampleProvider sampleProvider = new GarminHrvSummarySampleProvider(gbDevice, session);
for (final GarminHrvSummarySample sample : hrvSummarySamples) {
sample.setDevice(device);
sample.setUser(user);
}
sampleProvider.addSamples(hrvSummarySamples);
} catch (final Exception e) {
GB.toast(context, "Error saving HRV summary samples", Toast.LENGTH_LONG, GB.ERROR, e);
}
}
private void persistHrvValueSamples() {
if (hrvValueSamples.isEmpty()) {
return;
}
LOG.debug("Will persist {} HRV value samples", hrvValueSamples.size());
try (DBHandler handler = GBApplication.acquireDB()) {
final DaoSession session = handler.getDaoSession();
final Device device = DBHelper.getDevice(gbDevice, session);
final User user = DBHelper.getUser(session);
final GarminHrvValueSampleProvider sampleProvider = new GarminHrvValueSampleProvider(gbDevice, session);
for (final GarminHrvValueSample sample : hrvValueSamples) {
sample.setDevice(device);
sample.setUser(user);
}
sampleProvider.addSamples(hrvValueSamples);
} catch (final Exception e) {
GB.toast(context, "Error saving HRV value samples", Toast.LENGTH_LONG, GB.ERROR, e);
}
}
private void persistSpo2Samples() {
if (spo2samples.isEmpty()) {
return;

View File

@ -253,6 +253,22 @@ public class GlobalFITMessage {
public static GlobalFITMessage SLEEP_STATS = new GlobalFITMessage(346, "SLEEP_STATS", Arrays.asList(
));
public static GlobalFITMessage HRV_SUMMARY = new GlobalFITMessage(370, "HRV_SUMMARY", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT16, "weekly_average", 128, 0), // milliseconds, scaled by 128
new FieldDefinitionPrimitive(1, BaseType.UINT16, "last_night_average", 128, 0), // milliseconds, scaled by 128
new FieldDefinitionPrimitive(2, BaseType.UINT16, "last_night_5_min_high", 128, 0), // milliseconds, scaled by 128
new FieldDefinitionPrimitive(3, BaseType.UINT16, "baseline_low_upper", 128, 0), // milliseconds, scaled by 128
new FieldDefinitionPrimitive(4, BaseType.UINT16, "baseline_balanced_lower", 128, 0), // milliseconds, scaled by 128
new FieldDefinitionPrimitive(5, BaseType.UINT16, "baseline_balanced_upper", 128, 0), // milliseconds, scaled by 128
new FieldDefinitionPrimitive(6, BaseType.ENUM, "status", FieldDefinitionFactory.FIELD.HRV_STATUS),
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage HRV_VALUE = new GlobalFITMessage(371, "HRV_VALUE", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT16, "value", 128, 0), // milliseconds, scaled by 128
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static Map<Integer, GlobalFITMessage> KNOWN_MESSAGES = new HashMap<Integer, GlobalFITMessage>() {{
put(0, FILE_ID);
put(2, DEVICE_SETTINGS);
@ -280,6 +296,8 @@ public class GlobalFITMessage {
put(275, SLEEP_STAGE);
put(297, RESPIRATION_RATE);
put(346, SLEEP_STATS);
put(370, HRV_SUMMARY);
put(371, HRV_VALUE);
}};
private final int number;
private final String name;

View File

@ -22,13 +22,14 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.GlobalFITMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalSource;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionGoalType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionLanguage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionMeasurementSystem;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionSleepStage;
@ -230,11 +231,13 @@ public class FitCodeGen {
case DAY_OF_WEEK:
return DayOfWeek.class;
case FILE_TYPE:
return FieldDefinitionFileType.Type.class;
return FileType.FILETYPE.class;
case GOAL_SOURCE:
return FieldDefinitionGoalSource.Source.class;
case GOAL_TYPE:
return FieldDefinitionGoalType.Type.class;
case HRV_STATUS:
return FieldDefinitionHrvStatus.HrvStatus.class;
case MEASUREMENT_SYSTEM:
return FieldDefinitionMeasurementSystem.Type.class;
case TEMPERATURE:

View File

@ -1,9 +1,8 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
@ -16,47 +15,16 @@ public class FieldDefinitionFileType extends FieldDefinition {
@Override
public Object decode(ByteBuffer byteBuffer) {
int raw = (int) baseType.decode(byteBuffer, scale, offset);
return Type.fromId(raw) == null ? raw : Type.fromId(raw);
final FileType.FILETYPE fileType = FileType.FILETYPE.fromDataTypeSubType(128, raw);
return fileType == null ? raw : fileType;
}
@Override
public void encode(ByteBuffer byteBuffer, Object o) {
if (o instanceof Type) {
baseType.encode(byteBuffer, (((Type) o).getId()), scale, offset);
if (o instanceof FileType.FILETYPE) {
baseType.encode(byteBuffer, (((FileType.FILETYPE) o).getSubType()), scale, offset);
return;
}
baseType.encode(byteBuffer, o, scale, offset);
}
public enum Type {
settings(2),
activity(4), //FIT_TYPE_4 stands for activity directory
goals(11),
monitor(32), //FIT_TYPE_32
changelog(41), // FIT_TYPE_41 stands for changelog directory
metrics(44), //FIT_TYPE_41
sleep(49), //FIT_TYPE_49
;
private final int id;
Type(int i) {
this.id = i;
}
@Nullable
public static Type fromId(int id) {
for (Type type :
Type.values()) {
if (id == type.getId()) {
return type;
}
}
return null;
}
public int getId() {
return this.id;
}
}
}

View File

@ -0,0 +1,58 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions;
import androidx.annotation.Nullable;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.FieldDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.baseTypes.BaseType;
public class FieldDefinitionHrvStatus extends FieldDefinition {
public FieldDefinitionHrvStatus(final int localNumber, final int size, final BaseType baseType, final String name) {
super(localNumber, size, baseType, name, 1, 0);
}
@Override
public Object decode(final ByteBuffer byteBuffer) {
final int raw = (int) baseType.decode(byteBuffer, scale, offset);
return HrvStatus.fromId(raw);
}
@Override
public void encode(final ByteBuffer byteBuffer, final Object o) {
if (o instanceof HrvStatus) {
baseType.encode(byteBuffer, (((HrvStatus) o).getId()), scale, offset);
return;
}
baseType.encode(byteBuffer, o, scale, offset);
}
public enum HrvStatus {
NONE(0),
POOR(1),
LOW(2),
UNBALANCED(3),
BALANCED(4),
;
private final int id;
HrvStatus(final int i) {
id = i;
}
@Nullable
public static HrvStatus fromId(final int id) {
for (HrvStatus stage : HrvStatus.values()) {
if (id == stage.getId()) {
return stage;
}
}
return null;
}
public int getId() {
return id;
}
}
}

View File

@ -2,10 +2,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.FileType.FILETYPE;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionFileType.Type;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
@ -22,8 +22,8 @@ public class FitFileId extends RecordData {
}
@Nullable
public Type getType() {
return (Type) getFieldByNumber(0);
public FILETYPE getType() {
return (FILETYPE) getFieldByNumber(0);
}
@Nullable

View File

@ -0,0 +1,63 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.fieldDefinitions.FieldDefinitionHrvStatus.HrvStatus;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitHrvSummary extends RecordData {
public FitHrvSummary(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 370) {
throw new IllegalArgumentException("FitHrvSummary expects global messages of " + 370 + ", got " + globalNumber);
}
}
@Nullable
public Integer getWeeklyAverage() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Integer getLastNightAverage() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Integer getLastNight5MinHigh() {
return (Integer) getFieldByNumber(2);
}
@Nullable
public Integer getBaselineLowUpper() {
return (Integer) getFieldByNumber(3);
}
@Nullable
public Integer getBaselineBalancedLower() {
return (Integer) getFieldByNumber(4);
}
@Nullable
public Integer getBaselineBalancedUpper() {
return (Integer) getFieldByNumber(5);
}
@Nullable
public HrvStatus getStatus() {
return (HrvStatus) getFieldByNumber(6);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}

View File

@ -0,0 +1,32 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordDefinition;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.RecordHeader;
//
// WARNING: This class was auto-generated, please avoid modifying it directly.
// See nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.codegen.FitCodeGen
//
public class FitHrvValue extends RecordData {
public FitHrvValue(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 371) {
throw new IllegalArgumentException("FitHrvValue expects global messages of " + 371 + ", got " + globalNumber);
}
}
@Nullable
public Integer getValue() {
return (Integer) getFieldByNumber(0);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}

View File

@ -67,6 +67,10 @@ public class FitRecordDataFactory {
return new FitRespirationRate(recordDefinition, recordHeader);
case 346:
return new FitSleepStats(recordDefinition, recordHeader);
case 370:
return new FitHrvSummary(recordDefinition, recordHeader);
case 371:
return new FitHrvValue(recordDefinition, recordHeader);
}
return new RecordData(recordDefinition, recordHeader);