Colmi R0x: Add support for HRV

This commit is contained in:
Arjan Schrijver 2024-09-30 22:48:23 +02:00 committed by René Vögeli
parent 0a9da03618
commit 4939de47c1
11 changed files with 188 additions and 45 deletions

View File

@ -135,6 +135,7 @@ public class GBDaoGenerator {
addColmiStressSample(schema, user, device);
addColmiSleepSessionSample(schema, user, device);
addColmiSleepStageSample(schema, user, device);
addColmiHrvValueSample(schema, user, device);
addHuaweiActivitySample(schema, user, device);
@ -546,6 +547,13 @@ public class GBDaoGenerator {
return sleepStageSample;
}
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);
return hrvValueSample;
}
private static void addHeartRateProperties(Entity activitySample) {
activitySample.addIntProperty(SAMPLE_HEART_RATE).notNull().codeBeforeGetterAndSetter(OVERRIDE);
}

View File

@ -199,6 +199,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING = "heartrate_sleep_breathing_quality_monitoring";
public static final String PREF_SPO2_ALL_DAY_MONITORING = "spo2_all_day_monitoring_enabled";
public static final String PREF_SPO2_LOW_ALERT_THRESHOLD = "spo2_low_alert_threshold";
public static final String PREF_HRV_ALL_DAY_MONITORING = "hrv_all_day_monitoring_enabled";
public static final String PREF_AUTOHEARTRATE_SWITCH = "pref_autoheartrate_switch";
public static final String PREF_AUTOHEARTRATE_SLEEP = "pref_autoheartrate_sleep";

View File

@ -552,6 +552,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_HEARTRATE_SLEEP_BREATHING_QUALITY_MONITORING);
addPreferenceHandlerFor(PREF_SPO2_ALL_DAY_MONITORING);
addPreferenceHandlerFor(PREF_SPO2_LOW_ALERT_THRESHOLD);
addPreferenceHandlerFor(PREF_HRV_ALL_DAY_MONITORING);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_START);
addPreferenceHandlerFor(PREF_DO_NOT_DISTURB_NOAUTO_END);

View File

@ -22,9 +22,12 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import de.greenrobot.dao.query.QueryBuilder;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
@ -34,10 +37,12 @@ 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.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.ColmiHrvValueSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepSessionSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSleepStageSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiSpo2SampleDao;
@ -46,6 +51,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.HrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
@ -56,21 +62,23 @@ public abstract class AbstractColmiR0xCoordinator extends AbstractBLEDeviceCoord
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb;
final Long deviceId = device.getId();
qb = session.getColmiActivitySampleDao().queryBuilder();
qb.where(ColmiActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getColmiHeartRateSampleDao().queryBuilder();
qb.where(ColmiHeartRateSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getColmiSpo2SampleDao().queryBuilder();
qb.where(ColmiSpo2SampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getColmiStressSampleDao().queryBuilder();
qb.where(ColmiStressSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getColmiSleepSessionSampleDao().queryBuilder();
qb.where(ColmiSleepSessionSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
qb = session.getColmiSleepStageSampleDao().queryBuilder();
qb.where(ColmiSleepStageSampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
final Map<AbstractDao<?, ?>, Property> daoMap = new HashMap<AbstractDao<?, ?>, Property>() {{
put(session.getColmiActivitySampleDao(), ColmiActivitySampleDao.Properties.DeviceId);
put(session.getColmiHeartRateSampleDao(), ColmiHeartRateSampleDao.Properties.DeviceId);
put(session.getColmiSpo2SampleDao(), ColmiSpo2SampleDao.Properties.DeviceId);
put(session.getColmiStressSampleDao(), ColmiStressSampleDao.Properties.DeviceId);
put(session.getColmiSleepSessionSampleDao(), ColmiSleepSessionSampleDao.Properties.DeviceId);
put(session.getColmiSleepStageSampleDao(), ColmiSleepStageSampleDao.Properties.DeviceId);
put(session.getColmiHrvValueSampleDao(), ColmiHrvValueSampleDao.Properties.DeviceId);
}};
for (final Map.Entry<AbstractDao<?, ?>, Property> e : daoMap.entrySet()) {
e.getKey().queryBuilder()
.where(e.getValue().eq(deviceId))
.buildDelete().executeDeleteWithoutDetachingEntities();
}
}
@Override
@ -159,6 +167,11 @@ public abstract class AbstractColmiR0xCoordinator extends AbstractBLEDeviceCoord
return true;
}
@Override
public boolean supportsHrvMeasurement() {
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new ColmiActivitySampleProvider(device, session);
@ -174,6 +187,11 @@ public abstract class AbstractColmiR0xCoordinator extends AbstractBLEDeviceCoord
return new ColmiStressSampleProvider(device, session);
}
@Override
public TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(final GBDevice device, final DaoSession session) {
return new ColmiHrvValueSampleProvider(device, session);
}
@Override
public List<HeartRateCapability.MeasurementInterval> getHeartRateMeasurementIntervals() {
return Arrays.asList(

View File

@ -38,6 +38,8 @@ public class ColmiR0xConstants {
public static final byte CMD_PACKET_SIZE = 0x2f;
public static final byte CMD_AUTO_STRESS_PREF = 0x36;
public static final byte CMD_SYNC_STRESS = 0x37;
public static final byte CMD_AUTO_HRV_PREF = 0x38;
public static final byte CMD_SYNC_HRV = 0x39;
public static final byte CMD_SYNC_ACTIVITY = 0x43;
public static final byte CMD_FIND_DEVICE = 0x50;
public static final byte CMD_MANUAL_HEART_RATE = 0x69;

View File

@ -96,6 +96,17 @@ public class ColmiR0xPacketHandler {
support.evaluateGBDeviceEvent(eventUpdatePreferences);
}
public static void hrvSettings(ColmiR0xDeviceSupport support, byte[] value) {
boolean enabled = value[2] == 0x01;
LOG.info("Received HRV preference: {}", enabled ? "enabled" : "disabled");
GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences();
eventUpdatePreferences.withPreference(
DeviceSettingsPreferenceConst.PREF_HRV_ALL_DAY_MONITORING,
enabled
);
support.evaluateGBDeviceEvent(eventUpdatePreferences);
}
public static void goalsSettings(byte[] value) {
int steps = BLETypeConversions.toUint32(value[2], value[3], value[4], (byte) 0);
int calories = BLETypeConversions.toUint32(value[5], value[6], value[7], (byte) 0);
@ -410,4 +421,8 @@ public class ColmiR0xPacketHandler {
}
}
}
public static void historicalHRV(GBDevice device, Context context, byte[] value) {
}
}

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.ColmiHrvValueSample;
import nodomain.freeyourgadget.gadgetbridge.entities.ColmiHrvValueSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class ColmiHrvValueSampleProvider extends AbstractTimeSampleProvider<ColmiHrvValueSample> {
public ColmiHrvValueSampleProvider(final GBDevice device, final DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<ColmiHrvValueSample, ?> getSampleDao() {
return getSession().getColmiHrvValueSampleDao();
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return ColmiHrvValueSampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return ColmiHrvValueSampleDao.Properties.DeviceId;
}
@Override
public ColmiHrvValueSample createSample() {
return new ColmiHrvValueSample();
}
}

View File

@ -91,15 +91,6 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
deviceInfoProfile = new DeviceInfoProfile<>(this);
deviceInfoProfile.addListener(mListener);
addSupportedProfile(deviceInfoProfile);
// try (DBHandler db = GBApplication.acquireDB()) {
// db.getDatabase().execSQL("DROP TABLE IF EXISTS 'COLMI_ACTIVITY_SAMPLE'");
// db.getDatabase().execSQL("DROP TABLE IF EXISTS 'COLMI_HEART_RATE_SAMPLE'");
// db.getDatabase().execSQL("DROP TABLE IF EXISTS 'COLMI_SPO2_SAMPLE'");
// db.getDatabase().execSQL("DROP TABLE IF EXISTS 'COLMI_STRESS_SAMPLE'");
// } catch (Exception e) {
// LOG.error("Error acquiring database", e);
// }
}
@Override
@ -277,6 +268,9 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
case ColmiR0xConstants.CMD_AUTO_STRESS_PREF:
ColmiR0xPacketHandler.stressSettings(this, value);
break;
case ColmiR0xConstants.CMD_AUTO_HRV_PREF:
ColmiR0xPacketHandler.hrvSettings(this, value);
break;
case ColmiR0xConstants.CMD_SYNC_STRESS:
ColmiR0xPacketHandler.historicalStress(getDevice(), getContext(), value);
if (!getDevice().isBusy()) {
@ -295,6 +289,9 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
}
}
break;
case ColmiR0xConstants.CMD_SYNC_HRV:
ColmiR0xPacketHandler.historicalHRV(getDevice(), getContext(), value);
break;
case ColmiR0xConstants.CMD_FIND_DEVICE:
LOG.info("Received find device response: {}", StringUtils.bytesToHex(value));
break;
@ -367,6 +364,9 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
switch (value[1]) {
case ColmiR0xConstants.BIG_DATA_TYPE_SLEEP:
ColmiR0xPacketHandler.historicalSleep(getDevice(), getContext(), value);
fetchHistoryHRV();
// Signal history sync finished at this point, since older firmwares
// will not send anything back after requesting HRV history
fetchRecordedDataFinished();
break;
case ColmiR0xConstants.BIG_DATA_TYPE_SPO2:
@ -478,6 +478,12 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("Stress preference request sent: {}", StringUtils.bytesToHex(stressPrefsPacket));
sendWrite("stressPreferenceRequest", stressPrefsPacket);
break;
case DeviceSettingsPreferenceConst.PREF_HRV_ALL_DAY_MONITORING:
final boolean hrvEnabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_HRV_ALL_DAY_MONITORING, false);
byte[] hrvPrefsPacket = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_HRV_PREF, ColmiR0xConstants.PREF_WRITE, (byte) (hrvEnabled ? 0x01 : 0x00)});
LOG.info("HRV preference request sent: {}", StringUtils.bytesToHex(hrvPrefsPacket));
sendWrite("hrvPreferenceRequest", hrvPrefsPacket);
break;
}
}
@ -538,6 +544,9 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
request = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_SPO2_PREF, ColmiR0xConstants.PREF_READ});
LOG.info("Request SpO2 measurement setting from ring: {}", StringUtils.bytesToHex(request));
sendWrite("spo2SettingRequest", request);
request = buildPacket(new byte[]{ColmiR0xConstants.CMD_AUTO_HRV_PREF, ColmiR0xConstants.PREF_READ});
LOG.info("Request HRV measurement setting from ring: {}", StringUtils.bytesToHex(request));
sendWrite("hrvSettingRequest", request);
request = buildPacket(new byte[]{ColmiR0xConstants.CMD_GOALS, ColmiR0xConstants.PREF_READ});
LOG.info("Request goals from ring: {}", StringUtils.bytesToHex(request));
sendWrite("goalsSettingRequest", request);
@ -664,4 +673,13 @@ public class ColmiR0xDeviceSupport extends AbstractBTLEDeviceSupport {
LOG.info("Fetch historical sleep data request sent: {}", StringUtils.bytesToHex(sleepHistoryRequest));
sendCommand("sleepHistoryRequest", sleepHistoryRequest);
}
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));
sendWrite("hrvHistoryRequest", hrvHistoryRequest);
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#7E7E7E"
android:viewportHeight="24"
android:viewportWidth="24">
<path android:fillColor="#000000" android:pathData="M12,2c-5.33,4.55 -8,8.48 -8,11.8c0,4.98 3.8,8.2 8,8.2s8,-3.22 8,-8.2C20,10.48 17.33,6.55 12,2zM15,18H9v-2h6V18zM15,13h-2v2h-2v-2H9v-2h2V9h2v2h2V13z"/>
</vector>

View File

@ -693,6 +693,7 @@
<string name="busy_task_fetch_stress_data">Fetching stress data</string>
<string name="busy_task_fetch_pai_data">Fetching PAI data</string>
<string name="busy_task_fetch_spo2_data">Fetching SpO2 data</string>
<string name="busy_task_fetch_hrv_data">Fetching HRV data</string>
<string name="busy_task_fetch_hr_data">Fetching heart rate data</string>
<string name="busy_task_fetch_sleep_data">Fetching sleep data</string>
<string name="busy_task_fetch_sleep_respiratory_rate_data">Fetching sleep respiratory rate data</string>
@ -3355,4 +3356,10 @@
<string name="pref_dashboard_widget_today_yesterday_data_summary">Show data from yesterday dimmed between the current time and midnight</string>
<string name="pref_dashboard_widget_today_time_indicator_title">Current time indicator</string>
<string name="pref_dashboard_widget_today_time_indicator_summary">Show an indicator at the current time, to visually separate data from yesterday and today</string>
<string name="pref_dnd_follow_phone_title">Follow phone DND setting</string>
<string name="pref_dnd_follow_phone_summary">When DND is enabled or disabled on the phone, automatically toggle it on the device too</string>
<string name="inactivity_warnings_minimum_steps_title">Minimum amount of steps</string>
<string name="inactivity_warnings_minimum_steps_summary">Minimum amount of steps that need to be taken during the threshold minutes</string>
<string name="prefs_hrv_monitoring_title">HRV monitoring</string>
<string name="prefs_hrv_monitoring_description">Automatically monitor heart rate variability throughout the day</string>
</resources>

View File

@ -1,24 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="0"
android:entries="@array/prefs_heartrate_measurement_interval"
android:entryValues="@array/prefs_heartrate_measurement_interval_values"
android:icon="@drawable/ic_heartrate"
android:key="heartrate_measurement_interval"
android:summary="%s"
android:title="@string/prefs_title_heartrate_measurement_interval" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_mood_bad"
android:key="heartrate_stress_monitoring"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_stress_monitoring_description"
android:title="@string/prefs_stress_monitoring_title" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="spo2_all_day_monitoring_enabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_spo2_monitoring_description"
android:title="@string/prefs_spo2_monitoring_title" />
<ListPreference
android:defaultValue="0"
android:entries="@array/prefs_heartrate_measurement_interval"
android:entryValues="@array/prefs_heartrate_measurement_interval_values"
android:icon="@drawable/ic_heartrate"
android:key="heartrate_measurement_interval"
android:summary="%s"
android:title="@string/prefs_title_heartrate_measurement_interval" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_mood_bad"
android:key="heartrate_stress_monitoring"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_stress_monitoring_description"
android:title="@string/prefs_stress_monitoring_title" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/baseline_bloodtype_24"
android:key="spo2_all_day_monitoring_enabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_spo2_monitoring_description"
android:title="@string/prefs_spo2_monitoring_title" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_show_chart"
android:key="hrv_all_day_monitoring_enabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_hrv_monitoring_description"
android:title="@string/prefs_hrv_monitoring_title" />
</androidx.preference.PreferenceScreen>