1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-24 09:35:50 +01:00

Colmi R0x: Add support for HRV

This commit is contained in:
René Vögeli 2024-10-08 20:59:41 +02:00
parent 4939de47c1
commit c66467a915
5 changed files with 177 additions and 14 deletions

View File

@ -38,6 +38,14 @@ public class GBDaoGenerator {
private static final String SAMPLE_STEPS = "steps";
private static final String SAMPLE_RAW_KIND = "rawKind";
private static final String SAMPLE_HEART_RATE = "heartRate";
private static final String SAMPLE_HRV_WEEKLY_AVERAGE = "weeklyAverage";
private static final String SAMPLE_HRV_LAST_NIGHT_AVERAGE = "lastNightAverage";
private static final String SAMPLE_HRV_LAST_NIGHT_5MIN_HIGH = "lastNight5MinHigh";
private static final String SAMPLE_HRV_BASELINE_LOW_UPPER = "baselineLowUpper";
private static final String SAMPLE_HRV_BASELINE_BALANCED_LOWER = "baselineBalancedLower";
private static final String SAMPLE_HRV_BASELINE_BALANCED_UPPER = "baselineBalancedUpper";
private static final String SAMPLE_HRV_STATUS_NUM = "statusNum";
private static final String SAMPLE_HRV_VALUE = "value";
private static final String SAMPLE_TEMPERATURE = "temperature";
private static final String SAMPLE_TEMPERATURE_TYPE = "temperatureType";
private static final String SAMPLE_WEIGHT_KG = "weightKg";
@ -136,6 +144,7 @@ public class GBDaoGenerator {
addColmiSleepSessionSample(schema, user, device);
addColmiSleepStageSample(schema, user, device);
addColmiHrvValueSample(schema, user, device);
addColmiHrvSummarySample(schema, user, device);
addHuaweiActivitySample(schema, user, device);
@ -550,10 +559,23 @@ public class GBDaoGenerator {
private static Entity addColmiHrvValueSample(Schema schema, Entity user, Entity device) {
Entity hrvValueSample = addEntity(schema, "ColmiHrvValueSample");
addCommonTimeSampleProperties("AbstractHrvValueSample", hrvValueSample, user, device);
hrvValueSample.addIntProperty("value").notNull().codeBeforeGetter(OVERRIDE);
hrvValueSample.addIntProperty(SAMPLE_HRV_VALUE).notNull().codeBeforeGetter(OVERRIDE);
return hrvValueSample;
}
private static Entity addColmiHrvSummarySample(Schema schema, Entity user, Entity device) {
Entity hrvSummarySample = addEntity(schema, "ColmiHrvSummarySample");
addCommonTimeSampleProperties("AbstractHrvSummarySample", hrvSummarySample, user, device);
hrvSummarySample.addIntProperty(SAMPLE_HRV_WEEKLY_AVERAGE).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_LAST_NIGHT_AVERAGE).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_LAST_NIGHT_5MIN_HIGH).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_LOW_UPPER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_BALANCED_LOWER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_BALANCED_UPPER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_STATUS_NUM).codeBeforeGetter(OVERRIDE);
return hrvSummarySample;
}
private static void addHeartRateProperties(Entity activitySample) {
activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE);
}
@ -813,13 +835,13 @@ public class GBDaoGenerator {
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);
hrvSummarySample.addIntProperty(SAMPLE_HRV_WEEKLY_AVERAGE).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_LAST_NIGHT_AVERAGE).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_LAST_NIGHT_5MIN_HIGH).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_LOW_UPPER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_BALANCED_LOWER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_BASELINE_BALANCED_UPPER).codeBeforeGetter(OVERRIDE);
hrvSummarySample.addIntProperty(SAMPLE_HRV_STATUS_NUM).codeBeforeGetter(OVERRIDE);
return hrvSummarySample;
}

View File

@ -37,11 +37,13 @@ import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiHrvSummarySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiHrvValueSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHeartRateSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvValueSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepSessionSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepStageSampleDao;
@ -51,6 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
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;
@ -71,6 +74,7 @@ public abstract class AbstractColmiR0xCoordinator extends AbstractBLEDeviceCoord
put(session.getColmiStressSampleDao(), ColmiStressSampleDao.Properties.DeviceId);
put(session.getColmiSleepSessionSampleDao(), ColmiSleepSessionSampleDao.Properties.DeviceId);
put(session.getColmiSleepStageSampleDao(), ColmiSleepStageSampleDao.Properties.DeviceId);
put(session.getColmiHrvSummarySampleDao(), ColmiHrvSummarySampleDao.Properties.DeviceId);
put(session.getColmiHrvValueSampleDao(), ColmiHrvValueSampleDao.Properties.DeviceId);
}};
@ -187,6 +191,11 @@ public abstract class AbstractColmiR0xCoordinator extends AbstractBLEDeviceCoord
return new ColmiStressSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HrvSummarySample> getHrvSummarySampleProvider(GBDevice device, DaoSession session) {
return new ColmiHrvSummarySampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(final GBDevice device, final DaoSession session) {
return new ColmiHrvValueSampleProvider(device, session);

View File

@ -37,12 +37,14 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiHeartRateSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiHrvValueSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSleepSessionSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSleepStageSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.colmi.samples.ColmiStressSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHeartRateSample;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepSessionSample;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepStageSample;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSpo2Sample;
@ -422,7 +424,59 @@ public class ColmiR0xPacketHandler {
}
}
public static void historicalHRV(GBDevice device, Context context, byte[] value) {
public static void historicalHRV(GBDevice device, Context context, byte[] value, int daysAgo) {
LOG.info("Received HRV history sync packet: {}", StringUtils.bytesToHex(value));
int hrvPacketNr = value[1] & 0xff;
if (hrvPacketNr == 0xff) {
LOG.info("Empty HRV history, sync aborted");
device.unsetBusyTask();
device.sendDeviceUpdateIntent(context);
} else if (hrvPacketNr == 0) {
int packetsTotalNr = value[2];
LOG.info("HRV history packet {} out of total {}", hrvPacketNr, packetsTotalNr);
} else {
LOG.info("HRV history packet {}", hrvPacketNr);
Calendar sampleCal = Calendar.getInstance();
if (daysAgo != 0) {
sampleCal.add(Calendar.DAY_OF_MONTH, 0 - daysAgo);
sampleCal.set(Calendar.HOUR_OF_DAY, 0);
sampleCal.set(Calendar.MINUTE, 0);
}
sampleCal.set(Calendar.SECOND, 0);
sampleCal.set(Calendar.MILLISECOND, 0);
int startValue = hrvPacketNr == 1 ? 3 : 2; // packet 1 contains something in byte 2
int minutesInPreviousPackets = 0;
if (hrvPacketNr > 1) {
minutesInPreviousPackets = 12 * 30; // packet 1
minutesInPreviousPackets += (hrvPacketNr - 2) * 13 * 30;
}
for (int i = startValue; i < value.length - 1; i++) {
if (value[i] != 0x00) {
// Determine time of day
int minuteOfDay = minutesInPreviousPackets + (i - startValue) * 30;
sampleCal.set(Calendar.HOUR_OF_DAY, minuteOfDay / 60);
sampleCal.set(Calendar.MINUTE, minuteOfDay % 60);
LOG.info("Value {} is {} ms, time of day is {}", i, value[i] & 0xff, sampleCal.getTime());
// Build sample object and save in database
try (DBHandler db = GBApplication.acquireDB()) {
ColmiHrvValueSampleProvider sampleProvider = new ColmiHrvValueSampleProvider(device, db.getDaoSession());
Long userId = DBHelper.getUser(db.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(device, db.getDaoSession()).getId();
ColmiHrvValueSample gbSample = new ColmiHrvValueSample();
gbSample.setDeviceId(deviceId);
gbSample.setUserId(userId);
gbSample.setTimestamp(sampleCal.getTimeInMillis());
gbSample.setValue(value[i] & 0xff);
sampleProvider.addSample(gbSample);
} catch (Exception e) {
LOG.error("Error acquiring database for recording HRV samples", e);
}
}
}
if (hrvPacketNr == 4) {
device.unsetBusyTask();
device.sendDeviceUpdateIntent(context);
}
}
}
}

View File

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

View File

@ -290,7 +290,16 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
}
break;
case ColmiR0xConstants.CMD_SYNC_HRV:
ColmiR0xPacketHandler.historicalHRV(getDevice(), getContext(), value);
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_hrv_data));
ColmiR0xPacketHandler.historicalHRV(getDevice(), getContext(), value, daysAgo);
if (!getDevice().isBusy()) {
if (daysAgo < 6) {
daysAgo++;
fetchHistoryHRV();
} else {
fetchRecordedDataFinished();
}
}
break;
case ColmiR0xConstants.CMD_FIND_DEVICE:
LOG.info("Received find device response: {}", StringUtils.bytesToHex(value));
@ -364,7 +373,10 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
switch (value[1]) {
case ColmiR0xConstants.BIG_DATA_TYPE_SLEEP:
ColmiR0xPacketHandler.historicalSleep(getDevice(), getContext(), value);
daysAgo = 0;
fetchHistoryHRV();
// Signal history sync finished at this point, since older firmwares
// will not send anything back after requesting HRV history
fetchRecordedDataFinished();
@ -675,11 +687,21 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
}
private void fetchHistoryHRV() {
getDevice().setBusyTask(getContext().getString(R.string.busy_task_fetch_hrv_data));
getDevice().sendDeviceUpdateIntent(getContext());
syncingDay = Calendar.getInstance();
byte[] hrvHistoryRequest = buildPacket(new byte[]{ColmiR0xConstants.CMD_SYNC_HRV});
LOG.info("Fetch historical HRV data request sent: {}", StringUtils.bytesToHex(hrvHistoryRequest));
if (daysAgo != 0) {
syncingDay.add(Calendar.DAY_OF_MONTH, 0 - daysAgo);
syncingDay.set(Calendar.HOUR_OF_DAY, 0);
syncingDay.set(Calendar.MINUTE, 0);
}
syncingDay.set(Calendar.SECOND, 0);
syncingDay.set(Calendar.MILLISECOND, 0);
ByteBuffer hrvHistoryRequestBB = ByteBuffer.allocate(5);
hrvHistoryRequestBB.order(ByteOrder.LITTLE_ENDIAN);
hrvHistoryRequestBB.put(0, ColmiR0xConstants.CMD_SYNC_HRV);
hrvHistoryRequestBB.putInt(1, daysAgo);
byte[] hrvHistoryRequest = buildPacket(hrvHistoryRequestBB.array());
LOG.info("Fetch historical HRV data request sent ({}): {}", syncingDay.getTime(), StringUtils.bytesToHex(hrvHistoryRequest));
sendWrite("hrvHistoryRequest", hrvHistoryRequest);
}
}