Garmin: Add nap support

This commit is contained in:
José Rebelo 2025-03-08 18:03:15 +00:00
parent 624fb5cf64
commit 00b5420fbe
7 changed files with 175 additions and 1 deletions

View File

@ -54,7 +54,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
final Schema schema = new Schema(96, MAIN_PACKAGE + ".entities");
final Schema schema = new Schema(97, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -132,6 +132,7 @@ public class GBDaoGenerator {
addGarminRestingMetabolicRateSample(schema, user, device);
addGarminSleepStatsSample(schema, user, device);
addGarminIntensityMinutesSample(schema, user, device);
addGarminNapSample(schema, user, device);
addPendingFile(schema, user, device);
addWena3EnergySample(schema, user, device);
addWena3BehaviorSample(schema, user, device);
@ -925,6 +926,13 @@ public class GBDaoGenerator {
return sample;
}
private static Entity addGarminNapSample(Schema schema, Entity user, Entity device) {
Entity sample = addEntity(schema, "GarminNapSample");
addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device);
sample.addLongProperty("endTimestamp").notNull();
return sample;
}
private static Entity addPendingFile(Schema schema, Entity user, Entity device) {
Entity pendingFile = addEntity(schema, "PendingFile");
pendingFile.setJavaDoc(

View File

@ -22,6 +22,7 @@ import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
@ -31,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminNapSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
@ -182,6 +184,22 @@ public class GarminActivitySampleProvider extends AbstractSampleProvider<GarminA
}
}
// Overlap nap samples as light sleep
// TODO: Dedicated nap support in Gb?
final GarminNapSampleProvider napSampleProvider = new GarminNapSampleProvider(getDevice(), getSession());
final List<GarminNapSample> napSamples = new ArrayList<>(napSampleProvider.getAllSamples(
timestamp_from * 1000L,
timestamp_to * 1000L
));
final GarminNapSample lastNapSample = napSampleProvider.getLastSampleBefore(timestamp_from * 1000L);
if (lastNapSample != null) {
napSamples.add(lastNapSample);
}
for (final GarminNapSample napSample : napSamples) {
stagesMap.put(napSample.getTimestamp(), ActivityKind.UNKNOWN);
stagesMap.put(napSample.getEndTimestamp(), ActivityKind.LIGHT_SLEEP);
}
if (!stagesMap.isEmpty()) {
for (final GarminActivitySample sample : samples) {
final long ts = sample.getTimestamp() * 1000L;

View File

@ -0,0 +1,56 @@
/* Copyright (C) 2025 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.GarminNapSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminNapSampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class GarminNapSampleProvider extends AbstractTimeSampleProvider<GarminNapSample> {
public GarminNapSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<GarminNapSample, ?> getSampleDao() {
return getSession().getGarminNapSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return GarminNapSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return GarminNapSampleDao.Properties.DeviceId;
}
@Override
public GarminNapSample createSample() {
return new GarminNapSample();
}
}

View File

@ -30,6 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHeartRateRestin
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvSummarySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminHrvValueSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminIntensityMinutesSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminNapSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRespiratoryRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRestingMetabolicRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminSleepStatsSampleProvider;
@ -46,6 +47,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.GarminBodyEnergySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminEventSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHeartRateRestingSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminIntensityMinutesSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminNapSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminRestingMetabolicRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.GarminHrvValueSample;
@ -70,6 +72,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoring;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoringHrData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitMonitoringInfo;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitNap;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitPhysiologicalMetrics;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRecord;
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.fit.messages.FitRespirationRate;
@ -100,6 +103,7 @@ public class FitImporter {
private final List<GarminEventSample> events = new ArrayList<>();
private final List<GarminSleepStatsSample> sleepStatsSamples = new ArrayList<>();
private final List<GarminSleepStageSample> sleepStageSamples = new ArrayList<>();
private final List<GarminNapSample> napSamples = new ArrayList<>();
private final List<GarminHrvSummarySample> hrvSummarySamples = new ArrayList<>();
private final List<GarminHrvValueSample> hrvValueSamples = new ArrayList<>();
private final List<GarminRestingMetabolicRateSample> restingMetabolicRateSamples = new ArrayList<>();
@ -194,6 +198,16 @@ public class FitImporter {
sample.setTimestamp(ts * 1000L);
sample.setStage(stage.getId());
sleepStageSamples.add(sample);
} else if (record instanceof FitNap) {
final FitNap nap = (FitNap) record;
if (nap.getStartTimestamp() == null || nap.getEndTimestamp() == null) {
continue;
}
LOG.trace("Nap at {}: from {} to {}", ts, nap.getStartTimestamp(), nap.getEndTimestamp());
final GarminNapSample sample = new GarminNapSample();
sample.setTimestamp(nap.getStartTimestamp() * 1000L);
sample.setEndTimestamp(nap.getEndTimestamp() * 1000L);
napSamples.add(sample);
} else if (record instanceof FitMonitoring) {
LOG.trace("Monitoring at {}: {}", ts, record);
final FitMonitoring monitoringRecord = (FitMonitoring) record;
@ -382,6 +396,7 @@ public class FitImporter {
case SLEEP:
persistAbstractSamples(events, new GarminEventSampleProvider(gbDevice, session));
persistAbstractSamples(sleepStatsSamples, new GarminSleepStatsSampleProvider(gbDevice, session));
persistAbstractSamples(napSamples, new GarminNapSampleProvider(gbDevice, session));
// We may have samples, but not sleep samples - #4048
// 0 unmeasurable, 1 awake
@ -453,6 +468,7 @@ public class FitImporter {
events.clear();
sleepStatsSamples.clear();
sleepStageSamples.clear();
napSamples.clear();
hrvSummarySamples.clear();
hrvValueSamples.clear();
restingMetabolicRateSamples.clear();

View File

@ -490,6 +490,17 @@ public class GlobalFITMessage {
new FieldDefinitionPrimitive(253, BaseType.UINT32, "timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP)
));
public static GlobalFITMessage NAP = new GlobalFITMessage(412, "NAP", Arrays.asList(
new FieldDefinitionPrimitive(0, BaseType.UINT32, "start_timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP),
new FieldDefinitionPrimitive(1, BaseType.SINT16, "unknown_1"), // 0
new FieldDefinitionPrimitive(2, BaseType.UINT32, "end_timestamp", FieldDefinitionFactory.FIELD.TIMESTAMP),
new FieldDefinitionPrimitive(3, BaseType.SINT16, "unknown_3"), // 0
new FieldDefinitionPrimitive(4, BaseType.ENUM, "unknown_4"), // 8
new FieldDefinitionPrimitive(6, BaseType.ENUM, "unknown_6"), // 0
new FieldDefinitionPrimitive(7, BaseType.UINT32, "timestamp_7", FieldDefinitionFactory.FIELD.TIMESTAMP),
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);
@ -531,6 +542,7 @@ public class GlobalFITMessage {
put(371, HRV_VALUE);
put(397, SKIN_TEMP_RAW);
put(398, SKIN_TEMP_OVERNIGHT);
put(412, NAP);
}};
private final int number;

View File

@ -0,0 +1,62 @@
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 FitNap extends RecordData {
public FitNap(final RecordDefinition recordDefinition, final RecordHeader recordHeader) {
super(recordDefinition, recordHeader);
final int globalNumber = recordDefinition.getGlobalFITMessage().getNumber();
if (globalNumber != 412) {
throw new IllegalArgumentException("FitNap expects global messages of " + 412 + ", got " + globalNumber);
}
}
@Nullable
public Long getStartTimestamp() {
return (Long) getFieldByNumber(0);
}
@Nullable
public Integer getUnknown1() {
return (Integer) getFieldByNumber(1);
}
@Nullable
public Long getEndTimestamp() {
return (Long) getFieldByNumber(2);
}
@Nullable
public Integer getUnknown3() {
return (Integer) getFieldByNumber(3);
}
@Nullable
public Integer getUnknown4() {
return (Integer) getFieldByNumber(4);
}
@Nullable
public Integer getUnknown6() {
return (Integer) getFieldByNumber(6);
}
@Nullable
public Long getTimestamp7() {
return (Long) getFieldByNumber(7);
}
@Nullable
public Long getTimestamp() {
return (Long) getFieldByNumber(253);
}
}

View File

@ -95,6 +95,8 @@ public class FitRecordDataFactory {
return new FitSkinTempRaw(recordDefinition, recordHeader);
case 398:
return new FitSkinTempOvernight(recordDefinition, recordHeader);
case 412:
return new FitNap(recordDefinition, recordHeader);
}
return new RecordData(recordDefinition, recordHeader);