From c66467a9158782adafb32513bf585be6b77365d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20V=C3=B6geli?= Date: Tue, 8 Oct 2024 20:59:41 +0200 Subject: [PATCH] Colmi R0x: Add support for HRV --- .../gadgetbridge/daogen/GBDaoGenerator.java | 38 +++++++++--- .../colmi/AbstractColmiR0xCoordinator.java | 9 +++ .../devices/colmi/ColmiR0xPacketHandler.java | 58 ++++++++++++++++++- .../ColmiHrvSummarySampleProvider.java | 56 ++++++++++++++++++ .../devices/colmi/ColmiR0xDeviceSupport.java | 30 ++++++++-- 5 files changed, 177 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiHrvSummarySampleProvider.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index bc31fa611..322f25086 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -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; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/AbstractColmiR0xCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/AbstractColmiR0xCoordinator.java index db7150c19..1a6136c6d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/AbstractColmiR0xCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/AbstractColmiR0xCoordinator.java @@ -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 getHrvSummarySampleProvider(GBDevice device, DaoSession session) { + return new ColmiHrvSummarySampleProvider(device, session); + } + @Override public TimeSampleProvider getHrvValueSampleProvider(final GBDevice device, final DaoSession session) { return new ColmiHrvValueSampleProvider(device, session); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java index 7e85ec05c..6238cd55b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/ColmiR0xPacketHandler.java @@ -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); + } + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiHrvSummarySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiHrvSummarySampleProvider.java new file mode 100644 index 000000000..99214f8c6 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/colmi/samples/ColmiHrvSummarySampleProvider.java @@ -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 . */ +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 { + public ColmiHrvSummarySampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java index e6f9fe846..646777ce4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/colmi/ColmiR0xDeviceSupport.java @@ -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); } }