diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index fabbb0ba91..85b5cc473b 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -46,6 +46,8 @@ public class GBDaoGenerator { 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_SPO2 = "spo2"; + private static final String SAMPLE_STRESS = "stress"; private static final String SAMPLE_TEMPERATURE = "temperature"; private static final String SAMPLE_TEMPERATURE_TYPE = "temperatureType"; private static final String SAMPLE_WEIGHT_KG = "weightKg"; @@ -54,7 +56,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - final Schema schema = new Schema(97, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(98, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -157,6 +159,9 @@ public class GBDaoGenerator { addHuaweiActivitySample(schema, user, device); + addUltrahumanActivitySample(schema, user, device); + addUltrahumanDeviceStateSample(schema, user, device); + Entity huaweiWorkoutSummary = addHuaweiWorkoutSummarySample(schema, user, device); addHuaweiWorkoutDataSample(schema, huaweiWorkoutSummary); addHuaweiWorkoutPaceSample(schema, huaweiWorkoutSummary); @@ -181,6 +186,13 @@ public class GBDaoGenerator { addActivitySummary(schema, user, device); addBatteryLevel(schema, device); + + addGenericHeartRateSample(schema, user, device); + addGenericSpo2Sample(schema, user, device); + addGenericStressSample(schema, user, device); + addGenericHrvValueSample(schema, user, device); + addGenericTemperatureSample(schema, user, device); + new DaoGenerator().generateAll(schema, "app/src/main/java"); } @@ -328,7 +340,7 @@ public class GBDaoGenerator { Entity stressSample = addEntity(schema, "HuamiStressSample"); addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); stressSample.addIntProperty("typeNum").notNull().codeBeforeGetter(OVERRIDE); - stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); + stressSample.addIntProperty(SAMPLE_STRESS).notNull().codeBeforeGetter(OVERRIDE); return stressSample; } @@ -336,7 +348,7 @@ public class GBDaoGenerator { Entity spo2sample = addEntity(schema, "HuamiSpo2Sample"); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); spo2sample.addIntProperty("typeNum").notNull().codeBeforeGetter(OVERRIDE); - spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); + spo2sample.addIntProperty(SAMPLE_SPO2).notNull().codeBeforeGetter(OVERRIDE); return spo2sample; } @@ -400,8 +412,8 @@ public class GBDaoGenerator { activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); addHeartRateProperties(activitySample); - activitySample.addIntProperty("stress"); - activitySample.addIntProperty("spo2"); + activitySample.addIntProperty(SAMPLE_STRESS); + activitySample.addIntProperty(SAMPLE_SPO2); return activitySample; } @@ -486,14 +498,14 @@ public class GBDaoGenerator { private static Entity addCmfStressSample(Schema schema, Entity user, Entity device) { Entity stressSample = addEntity(schema, "CmfStressSample"); addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); - stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); + stressSample.addIntProperty(SAMPLE_STRESS).notNull().codeBeforeGetter(OVERRIDE); return stressSample; } private static Entity addCmfSpo2Sample(Schema schema, Entity user, Entity device) { Entity spo2sample = addEntity(schema, "CmfSpo2Sample"); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); - spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); + spo2sample.addIntProperty(SAMPLE_SPO2).notNull().codeBeforeGetter(OVERRIDE); return spo2sample; } @@ -551,14 +563,14 @@ public class GBDaoGenerator { private static Entity addColmiStressSample(Schema schema, Entity user, Entity device) { Entity stressSample = addEntity(schema, "ColmiStressSample"); addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); - stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); + stressSample.addIntProperty(SAMPLE_STRESS).notNull().codeBeforeGetter(OVERRIDE); return stressSample; } private static Entity addColmiSpo2Sample(Schema schema, Entity user, Entity device) { Entity spo2sample = addEntity(schema, "ColmiSpo2Sample"); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); - spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); + spo2sample.addIntProperty(SAMPLE_SPO2).notNull().codeBeforeGetter(OVERRIDE); return spo2sample; } @@ -759,7 +771,7 @@ public class GBDaoGenerator { private static Entity addHybridHRSpo2Sample(Schema schema, Entity user, Entity device) { Entity spo2sample = addEntity(schema, "HybridHRSpo2Sample"); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); - spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); + spo2sample.addIntProperty(SAMPLE_SPO2).notNull().codeBeforeGetter(OVERRIDE); return spo2sample; } @@ -834,7 +846,7 @@ public class GBDaoGenerator { private static Entity addGarminStressSample(Schema schema, Entity user, Entity device) { Entity stressSample = addEntity(schema, "GarminStressSample"); addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); - stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); + stressSample.addIntProperty(SAMPLE_STRESS).notNull().codeBeforeGetter(OVERRIDE); return stressSample; } @@ -848,7 +860,7 @@ public class GBDaoGenerator { private static Entity addGarminSpo2Sample(Schema schema, Entity user, Entity device) { Entity spo2sample = addEntity(schema, "GarminSpo2Sample"); addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); - spo2sample.addIntProperty("spo2").notNull().codeBeforeGetter(OVERRIDE); + spo2sample.addIntProperty(SAMPLE_SPO2).notNull().codeBeforeGetter(OVERRIDE); return spo2sample; } @@ -1337,7 +1349,7 @@ public class GBDaoGenerator { Entity stressSample = addEntity(schema, "Wena3StressSample"); addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); stressSample.addIntProperty("typeNum").notNull().codeBeforeGetter(OVERRIDE); - stressSample.addIntProperty("stress").notNull().codeBeforeGetter(OVERRIDE); + stressSample.addIntProperty(SAMPLE_STRESS).notNull().codeBeforeGetter(OVERRIDE); return stressSample; } @@ -1613,6 +1625,31 @@ public class GBDaoGenerator { return workoutSectionsSample; } + private static Entity addUltrahumanActivitySample(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "UltrahumanActivitySample"); + + addCommonActivitySampleProperties("AbstractUltrahumanActivitySample", sample, user, device); + sample.addIntProperty(SAMPLE_RAW_KIND).notNull(); + sample.addIntProperty(SAMPLE_HEART_RATE).notNull(); + sample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull(); + sample.addIntProperty(SAMPLE_STEPS).notNull(); + + return sample; + } + + private static Entity addUltrahumanDeviceStateSample(Schema schema, Entity user, Entity device) { + Entity sample = addEntity(schema, "UltrahumanDeviceStateSample"); + + addCommonTimeSampleProperties("AbstractTimeSample", sample, user, device); + sample.addByteArrayProperty(SAMPLE_RAW_KIND).notNull(); + + sample.addIntProperty("batteryLevel"); + sample.addIntProperty("deviceState"); + sample.addIntProperty("deviceTemperature"); + + return sample; + } + private static Entity addHuaweiDictData(Schema schema, Entity user, Entity device) { Entity dictData = addEntity(schema, "HuaweiDictData"); @@ -1685,4 +1722,40 @@ public class GBDaoGenerator { sample.addFloatProperty(SAMPLE_WEIGHT_KG).notNull().codeBeforeGetter(OVERRIDE); return sample; } + + private static Entity addGenericHeartRateSample(Schema schema, Entity user, Entity device) { + Entity heartRateSample = addEntity(schema, "GenericHeartRateSample"); + addCommonTimeSampleProperties("AbstractHeartRateSample", heartRateSample, user, device); + heartRateSample.addIntProperty(SAMPLE_HEART_RATE).notNull(); + return heartRateSample; + } + + private static Entity addGenericHrvValueSample(Schema schema, Entity user, Entity device) { + Entity hrvValueSample = addEntity(schema, "GenericHrvValueSample"); + addCommonTimeSampleProperties("AbstractHrvValueSample", hrvValueSample, user, device); + hrvValueSample.addIntProperty(SAMPLE_HRV_VALUE).notNull(); + return hrvValueSample; + } + + private static Entity addGenericSpo2Sample(Schema schema, Entity user, Entity device) { + Entity spo2sample = addEntity(schema, "GenericSpo2Sample"); + addCommonTimeSampleProperties("AbstractSpo2Sample", spo2sample, user, device); + spo2sample.addIntProperty(SAMPLE_SPO2).notNull(); + return spo2sample; + } + + private static Entity addGenericStressSample(Schema schema, Entity user, Entity device) { + Entity stressSample = addEntity(schema, "GenericStressSample"); + addCommonTimeSampleProperties("AbstractStressSample", stressSample, user, device); + stressSample.addIntProperty(SAMPLE_STRESS).notNull(); + return stressSample; + } + + private static Entity addGenericTemperatureSample(Schema schema, Entity user, Entity device) { + Entity temperatureSample = addEntity(schema, "GenericTemperatureSample"); + addCommonTimeSampleProperties("AbstractTemperatureSample", temperatureSample, user, device); + temperatureSample.addFloatProperty(SAMPLE_TEMPERATURE).notNull(); + temperatureSample.addIntProperty(SAMPLE_TEMPERATURE_TYPE).notNull(); + return temperatureSample; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericHeartRateSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericHeartRateSampleProvider.java new file mode 100644 index 0000000000..e740453327 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericHeartRateSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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; + +import androidx.annotation.NonNull; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHeartRateSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHeartRateSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GenericHeartRateSampleProvider extends AbstractTimeSampleProvider { + public GenericHeartRateSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGenericHeartRateSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GenericHeartRateSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GenericHeartRateSampleDao.Properties.DeviceId; + } + + @Override + public GenericHeartRateSample createSample() { + return new GenericHeartRateSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericHrvValueSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericHrvValueSampleProvider.java new file mode 100644 index 0000000000..d20c4f21fa --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericHrvValueSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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; + +import androidx.annotation.NonNull; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHrvValueSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHrvValueSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GenericHrvValueSampleProvider extends AbstractTimeSampleProvider { + public GenericHrvValueSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGenericHrvValueSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GenericHrvValueSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GenericHrvValueSampleDao.Properties.DeviceId; + } + + @Override + public GenericHrvValueSample createSample() { + return new GenericHrvValueSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericSpo2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericSpo2SampleProvider.java new file mode 100644 index 0000000000..41c3767448 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericSpo2SampleProvider.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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; + +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.GenericSpo2Sample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericSpo2SampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GenericSpo2SampleProvider extends AbstractTimeSampleProvider { + public GenericSpo2SampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGenericSpo2SampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GenericSpo2SampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GenericSpo2SampleDao.Properties.DeviceId; + } + + @Override + public GenericSpo2Sample createSample() { + return new GenericSpo2Sample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericStressSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericStressSampleProvider.java new file mode 100644 index 0000000000..ddd052b3d4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericStressSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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; + +import androidx.annotation.NonNull; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericStressSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericStressSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GenericStressSampleProvider extends AbstractTimeSampleProvider { + public GenericStressSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGenericStressSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GenericStressSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GenericStressSampleDao.Properties.DeviceId; + } + + @Override + public GenericStressSample createSample() { + return new GenericStressSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericTemperatureSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericTemperatureSampleProvider.java new file mode 100644 index 0000000000..3643514db9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/GenericTemperatureSampleProvider.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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; + +import androidx.annotation.NonNull; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericTemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericTemperatureSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class GenericTemperatureSampleProvider extends AbstractTimeSampleProvider { + public GenericTemperatureSampleProvider(final GBDevice device, final DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getGenericTemperatureSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return GenericTemperatureSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return GenericTemperatureSampleDao.Properties.DeviceId; + } + + @Override + public GenericTemperatureSample createSample() { + return new GenericTemperatureSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanConstants.java new file mode 100644 index 0000000000..7083d1d76b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanConstants.java @@ -0,0 +1,39 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.ultrahuman; + +import java.util.UUID; + +public class UltrahumanConstants { + public static final String ACTION_AIRPLANE_MODE = "nodomain.freeyourgadget.gadgetbridge.ultrahuman.action.AIRPLANE_MODE"; + + public static final UUID UUID_SERVICE_COMMAND = UUID.fromString("86f65000-f706-58a0-95b2-1fb9261e4dc7"); + public static final UUID UUID_CHARACTERISTIC_COMMAND = UUID.fromString("86f65001-f706-58a0-95b2-1fb9261e4dc7"); + public static final UUID UUID_CHARACTERISTIC_RESPONSE = UUID.fromString("86f65002-f706-58a0-95b2-1fb9261e4dc7"); + + public static final UUID UUID_SERVICE_STATE = UUID.fromString("86f61000-f706-58a0-95b2-1fb9261e4dc7"); + public static final UUID UUID_CHARACTERISTIC_STATE = UUID.fromString("86f61001-f706-58a0-95b2-1fb9261e4dc7"); + + public static final byte OPERATION_SETTIME = 0x02; + public static final byte OPERATION_GET_RECORDINGS = 0x04; + public static final byte OPERATION_GET_FIRST_RECORDING_NR = 0x07; + public static final byte OPERATION_GET_LAST_RECORDING_NR = 0x08; + public static final byte OPERATION_PING = 0x59; + public static final byte OPERATION_ACTIVATE_AIRPLANE_MODE = 0x70; + public static final byte OPERATION_RESET = (byte) 0x98; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCardAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCardAction.java new file mode 100644 index 0000000000..cb1e15968f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCardAction.java @@ -0,0 +1,67 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.ultrahuman; + +import android.content.Context; +import android.content.Intent; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class UltrahumanDeviceCardAction implements DeviceCardAction { + + private final int Icon; + private final int Description; + private final int Question; + + private final String Action; + + public UltrahumanDeviceCardAction(int icon, int description, int question, String action) { + Icon = icon; + Description = description; + Question = question; + Action = action; + } + + @Override + public int getIcon(GBDevice device) { + return Icon; + } + + @Override + public String getDescription(GBDevice device, Context context) { + return context.getString(Description); + } + + @Override + public void onClick(GBDevice device, Context context) { + new MaterialAlertDialogBuilder(context) + .setTitle(Description) + .setMessage(Question) + .setIcon(Icon) + .setPositiveButton(android.R.string.yes, (dialog, whichButton) -> { + final Intent intent = new Intent(Action); + intent.putExtra(GBDevice.EXTRA_DEVICE, device); + context.sendBroadcast(intent); + }) + .setNegativeButton(android.R.string.no, null) + .show(); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCoordinator.java new file mode 100644 index 0000000000..32701fbd3f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/UltrahumanDeviceCoordinator.java @@ -0,0 +1,229 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.ultrahuman; + +import androidx.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCardAction; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericHeartRateSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericHrvValueSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericSpo2SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericTemperatureSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.samples.UltrahumanActivitySampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHeartRateSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHrvValueSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericSpo2SampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericStressSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericTemperatureSampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; +import nodomain.freeyourgadget.gadgetbridge.model.HeartRateSample; +import nodomain.freeyourgadget.gadgetbridge.model.HrvValueSample; +import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample; +import nodomain.freeyourgadget.gadgetbridge.model.StressSample; +import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.ultrahuman.UltrahumanDeviceSupport; + +public class UltrahumanDeviceCoordinator extends AbstractBLEDeviceCoordinator { + private static final Logger LOG = LoggerFactory.getLogger(UltrahumanDeviceCoordinator.class); + + @Override + public List getCustomActions() { + // TODO - use an airplane icon instead + DeviceCardAction airplaneMode = new UltrahumanDeviceCardAction(R.drawable.ic_circle, R.string.ultrahuman_airplane_mode_title, R.string.ultrahuman_airplane_mode_question, UltrahumanConstants.ACTION_AIRPLANE_MODE); + + return Collections.singletonList(airplaneMode); + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + final Long deviceId = device.getId(); + + final Map, Property> daoMap = new HashMap, Property>() {{ + put(session.getGenericHeartRateSampleDao(), GenericHeartRateSampleDao.Properties.DeviceId); + put(session.getGenericHrvValueSampleDao(), GenericHrvValueSampleDao.Properties.DeviceId); + put(session.getGenericSpo2SampleDao(), GenericSpo2SampleDao.Properties.DeviceId); + put(session.getGenericStressSampleDao(), GenericStressSampleDao.Properties.DeviceId); + put(session.getGenericTemperatureSampleDao(), GenericTemperatureSampleDao.Properties.DeviceId); + put(session.getUltrahumanActivitySampleDao(), UltrahumanActivitySampleDao.Properties.DeviceId); + put(session.getUltrahumanDeviceStateSampleDao(), UltrahumanDeviceStateSampleDao.Properties.DeviceId); + }}; + + for (final Map.Entry, Property> e : daoMap.entrySet()) { + e.getKey().queryBuilder() + .where(e.getValue().eq(deviceId)) + .buildDelete().executeDeleteWithoutDetachingEntities(); + } + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_smartring; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_smartring_disabled; + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_ultrahuma_ring_air; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return UltrahumanDeviceSupport.class; + } + + @Override + public String getManufacturer() { + return "Ultrahuman Healthcare Pvt Ltd"; + } + + @Override + public TimeSampleProvider getHrvValueSampleProvider(GBDevice device, DaoSession session) { + return new GenericHrvValueSampleProvider(device, session); + } + + @Override + public TimeSampleProvider getHeartRateMaxSampleProvider(GBDevice device, DaoSession session) { + return new GenericHeartRateSampleProvider(device, session); + } + + @Override + public SampleProvider getSampleProvider(final GBDevice device, final DaoSession session) { + return new UltrahumanActivitySampleProvider(device, session); + } + + @Override + public TimeSampleProvider getSpo2SampleProvider(GBDevice device, DaoSession session) { + return new GenericSpo2SampleProvider(device, session); + } + + @Override + public TimeSampleProvider getStressSampleProvider(GBDevice device, DaoSession session) { + return new GenericStressSampleProvider(device, session); + } + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("UH_[0-9A-F]{12}"); + } + + @Override + public int[] getSupportedDeviceSpecificSettings(GBDevice device) { + return new int[]{ + R.xml.devicesettings_time_sync + }; + } + + @Override + public TimeSampleProvider getTemperatureSampleProvider(GBDevice device, DaoSession session) { + return new GenericTemperatureSampleProvider(device, session); + } + + @Override + public boolean isExperimental() { + return true; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public boolean supportsContinuousTemperature() { + return true; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public boolean supportsHrvMeasurement() { + // TODO - needs getHrvSummarySampleProvider in addition to the implemented getHrvValueSampleProvider + return false; + } + + @Override + public boolean supportsManualHeartRateMeasurement(final GBDevice device) { + return false; + } + + @Override + public boolean supportsSleepMeasurement() { + return false; + } + + @Override + public boolean supportsSpo2(GBDevice device) { + return true; + } + + @Override + public boolean supportsStepCounter() { + return true; + } + + @Override + public boolean supportsTemperatureMeasurement() { + return true; + } + + @Override + public boolean supportsSpeedzones() { + return false; + } + + @Override + public boolean supportsStressMeasurement() { + return true; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/samples/UltrahumanActivitySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/samples/UltrahumanActivitySampleProvider.java new file mode 100644 index 0000000000..7f7b8e3113 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/samples/UltrahumanActivitySampleProvider.java @@ -0,0 +1,106 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.ultrahuman.samples; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public class UltrahumanActivitySampleProvider extends AbstractSampleProvider { + public UltrahumanActivitySampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + public static ActivityKind normalizeTypeStatic(int rawType) { + switch (rawType) { + case 5: + return ActivityKind.BREATHWORK; + case 6: + return ActivityKind.EXERCISE; + default: + return (rawType >= 100) ? ActivityKind.NOT_WORN : ActivityKind.UNKNOWN; + } + } + + public static float normalizeIntensityStatic(int rawIntensity) { + return rawIntensity / 150f; + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getUltrahumanActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return UltrahumanActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return UltrahumanActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return UltrahumanActivitySampleDao.Properties.DeviceId; + } + + @Override + public ActivityKind normalizeType(int rawType) { + return normalizeTypeStatic(rawType); + } + + @Override + public int toRawActivityKind(ActivityKind activityKind) { + switch (activityKind) { + case UNKNOWN: + return 1; + case BREATHWORK: + return 5; + case EXERCISE: + return 6; + case NOT_WORN: + return 100; + default: + return 0; + } + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return normalizeIntensityStatic(rawIntensity); + } + + @Override + public UltrahumanActivitySample createActivitySample() { + return new UltrahumanActivitySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/samples/UltrahumanDeviceStateSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/samples/UltrahumanDeviceStateSampleProvider.java new file mode 100644 index 0000000000..d9fc1bd22c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/ultrahuman/samples/UltrahumanDeviceStateSampleProvider.java @@ -0,0 +1,57 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.ultrahuman.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.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSample; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class UltrahumanDeviceStateSampleProvider extends AbstractTimeSampleProvider { + public UltrahumanDeviceStateSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @NonNull + @Override + public AbstractDao getSampleDao() { + return getSession().getUltrahumanDeviceStateSampleDao(); + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return UltrahumanDeviceStateSampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return UltrahumanDeviceStateSampleDao.Properties.DeviceId; + } + + @Override + public UltrahumanDeviceStateSample createSample() { + return new UltrahumanDeviceStateSample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractUltrahumanActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractUltrahumanActivitySample.java new file mode 100644 index 0000000000..091b4a500b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractUltrahumanActivitySample.java @@ -0,0 +1,43 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.entities; + +import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.samples.UltrahumanActivitySampleProvider; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; + +public abstract class AbstractUltrahumanActivitySample extends AbstractActivitySample { + @Override + public ActivityKind getKind() { + return UltrahumanActivitySampleProvider.normalizeTypeStatic(getRawKind()); + } + + @Override + public float getIntensity() { + return UltrahumanActivitySampleProvider.normalizeIntensityStatic(getRawIntensity()); + } + + @Override + public int getDistanceCm() { + return -1; + } + + @Override + public int getActiveCalories() { + return -1; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 7d8df74671..3c46ee1bfc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -284,6 +284,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.q30.SoundcoreQ30Co import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.um25.Coordinator.UM25Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.vesc.VescCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator; @@ -625,7 +626,8 @@ public enum DeviceType { CYCLING_SENSOR(CyclingSensorCoordinator.class), BLE_GATT_CLIENT(BleGattClientCoordinator.class), MARSTEK_B2500(MarstekB2500DeviceCoordinator.class), - TEST(TestDeviceCoordinator.class); + TEST(TestDeviceCoordinator.class), + ULTRAHUMAN_RING_AIR(UltrahumanDeviceCoordinator.class); private DeviceCoordinator coordinator; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/ultrahuman/UltrahumanDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/ultrahuman/UltrahumanDeviceSupport.java new file mode 100644 index 0000000000..0ab006692c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/ultrahuman/UltrahumanDeviceSupport.java @@ -0,0 +1,552 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.service.devices.ultrahuman; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.widget.Toast; + +import androidx.core.content.ContextCompat; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericHeartRateSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericHrvValueSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericSpo2SampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericStressSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.GenericTemperatureSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.samples.UltrahumanActivitySampleProvider; +import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.samples.UltrahumanDeviceStateSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHeartRateSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericHrvValueSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericSpo2Sample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericStressSample; +import nodomain.freeyourgadget.gadgetbridge.entities.GenericTemperatureSample; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.UltrahumanDeviceStateSample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfoProfile; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.GB; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; + +public class UltrahumanDeviceSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(UltrahumanDeviceSupport.class); + private BroadcastReceiver CommandReceiver; + private int FetchTo; + private int FetchFrom; + private int FetchCurrent; + + public UltrahumanDeviceSupport(DeviceType type) { + super(LOG); + + addSupportedService(UltrahumanConstants.UUID_SERVICE_COMMAND); + addSupportedService(UltrahumanConstants.UUID_SERVICE_STATE); + addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION); + + DeviceInfoProfile deviceProfile = new DeviceInfoProfile<>(this); + deviceProfile.addListener(new UltrahumanIntentListener()); + addSupportedProfile(deviceProfile); + } + + @Override + public void dispose() { + BroadcastReceiver receiver = CommandReceiver; + CommandReceiver = null; + + if (receiver != null) { + getContext().unregisterReceiver(receiver); + } + + super.dispose(); + } + + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + // reset to avoid funny states for re-connect + FetchTo = -1; + FetchFrom = -1; + FetchCurrent = -1; + + // required for DB + if (getDevice().getFirmwareVersion() == null) { + getDevice().setFirmwareVersion("N/A"); + getDevice().setFirmwareVersion2("N/A"); + } + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + if (CommandReceiver == null) { + CommandReceiver = new UltrahumanBroadcastReceiver(); + IntentFilter filter = new IntentFilter(); + filter.addAction(UltrahumanConstants.ACTION_AIRPLANE_MODE); + ContextCompat.registerReceiver(getContext(), CommandReceiver, filter, ContextCompat.RECEIVER_EXPORTED); + } + + // trying to read non-existing characteristics sometimes causes odd BLE failures + // so avoid DeviceInfoProfile.requestDeviceInfo + builder.read(getCharacteristic(DeviceInfoProfile.UUID_CHARACTERISTIC_HARDWARE_REVISION_STRING)); + builder.read(getCharacteristic(DeviceInfoProfile.UUID_CHARACTERISTIC_FIRMWARE_REVISION_STRING)); + builder.read(getCharacteristic(DeviceInfoProfile.UUID_CHARACTERISTIC_SERIAL_NUMBER_STRING)); + + // TODO - implement a "OPERATION_PING until answer" logic instead of waits + // sometimes the device is quite quick and other times it takes a while after + // BLE connectivity has been established before the services work reliably + + builder.wait(48 * 3); //BluetoothGatt.onConnectionUpdated typically reports interval=48 + + builder.read(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_STATE)); + + builder.wait(48 * 2); + + builder.notify(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_RESPONSE), true); + builder.notify(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_STATE), true); + + builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_PING}); + + boolean timeSync = getDevicePrefs().getBoolean(DeviceSettingsPreferenceConst.PREF_TIME_SYNC, true); + if (timeSync) { + builder.add(new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND))); + } + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + + return builder; + } + + @Override + public void onFetchRecordedData(int dataTypes) { + String title = getContext().getString(R.string.busy_task_fetch_activity_data); + GB.updateTransferNotification(title, "", true, 0, getContext()); + + String task = getContext().getString(R.string.busy_task_fetch_activity_data); + getDevice().setBusyTask(task); + getDevice().sendDeviceUpdateIntent(getContext()); + + FetchFrom = -1; + FetchTo = -1; + FetchCurrent = -1; + + TransactionBuilder builder = new TransactionBuilder("onFetchRecordedData"); + builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR}); + builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), new byte[]{UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR}); + + if (isConnected()) { + builder.queue(getQueue()); + } else { + GB.toast(getContext(), R.string.devicestatus_disconnected, Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public boolean onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { + if (super.onCharacteristicRead(gatt, characteristic, status)) { + return true; + } + + if (UltrahumanConstants.UUID_CHARACTERISTIC_STATE.equals(characteristic.getUuid())) { + if (status == BluetoothGatt.GATT_SUCCESS) { + return decodeDeviceState(characteristic.getValue()); + } + } + + return false; + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { + if (super.onCharacteristicChanged(gatt, characteristic)) { + return true; + } + + UUID characteristicUUID = characteristic.getUuid(); + byte[] raw = characteristic.getValue(); + + if (UltrahumanConstants.UUID_CHARACTERISTIC_STATE.equals(characteristicUUID)) { + return decodeDeviceState(raw); + } else if (UltrahumanConstants.UUID_CHARACTERISTIC_RESPONSE.equals(characteristicUUID)) { + if (raw.length < 5) { + LOG.error("received unexpectedly short BLE command response: {}", StringUtils.bytesToHex(raw)); + return false; + } + + byte op = raw[0]; + byte success = raw[1]; + byte result = raw[2]; + // ignore checksums for now - algorithm is unknown + //byte chk1 = raw[raw.length-1]; + //byte chk2 = raw[raw.length-2]; + + if (success == 0x00) { + LOG.debug("received BLE command response op={}, success={}, result={} : {}", op, success, result, StringUtils.bytesToHex(raw)); + } else if ((0xFF & success) == 0xFF) { + LOG.warn("received BLE command response op={}, success={}, result={} : {}", op, success, result, StringUtils.bytesToHex(raw)); + } else { + LOG.info("received BLE command response op={}, success={}, result={} : {}", op, success, result, StringUtils.bytesToHex(raw)); + } + + Context context = getContext(); + + switch (op) { + case UltrahumanConstants.OPERATION_GET_RECORDINGS: + return decodeRecordings(raw); + case UltrahumanConstants.OPERATION_PING: + if (raw[1] != 0x00) { + String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); + GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR); + } + return true; + case UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE: + switch (raw[2]) { + case 0x01: + GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_activated), Toast.LENGTH_LONG, GB.INFO); + return true; + case 0x02: + GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_on_charger), Toast.LENGTH_LONG, GB.ERROR); + return true; + case 0x03: + GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_too_full), Toast.LENGTH_LONG, GB.ERROR); + return true; + } + LOG.warn("set airplane mode - unknown error: {} ", StringUtils.bytesToHex(raw)); + GB.toast(context, context.getString(R.string.ultrahuman_airplane_mode_unknown), Toast.LENGTH_LONG, GB.ERROR); + return false; + + case UltrahumanConstants.OPERATION_GET_FIRST_RECORDING_NR: + if (raw[1] == 0x00 || raw[2] == 0x01) { + FetchFrom = BLETypeConversions.toUint16(raw, 3); + if (FetchTo != -1) { + fetchRecordingActually(); + } + return true; + } else { + String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); + GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR); + fetchRecordedDataFinished(); + } + return false; + + case UltrahumanConstants.OPERATION_GET_LAST_RECORDING_NR: + if (raw[1] == 0x00 && raw[2] == 0x01) { + FetchTo = BLETypeConversions.toUint16(raw, 3); + if (FetchFrom != -1) { + fetchRecordingActually(); + } + return true; + } else { + String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); + GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR); + fetchRecordedDataFinished(); + } + return false; + + case UltrahumanConstants.OPERATION_SETTIME: + if (raw[1] != 0x00 || raw[2] != 0x01) { + String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); + GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR); + } + return true; + default: + LOG.error("unhandled BLE command response: {} ", StringUtils.bytesToHex(raw)); + return false; + } + } + + LOG.error("unhandled characteristics {} - {} ", characteristicUUID, StringUtils.bytesToHex(raw)); + return false; + } + + + @Override + public boolean useAutoConnect() { + return false; + } + + private void fetchRecordingActually() { + sendCommand("GetRecordingsCommand", new byte[]{UltrahumanConstants.OPERATION_GET_RECORDINGS, (byte) (FetchFrom & 0xFF), (byte) ((FetchFrom >> 8) & 0xFF)}); + } + + private boolean decodeRecordings(byte[] raw) { + if (raw[1] != 0) { + if ((raw[1] & 0xFF) == 0xEE) { + LOG.warn("no historic data recorded"); + } else { + String message = getContext().getString(R.string.ultrahuman_unhandled_error_response, raw[1], raw[0]); + GB.toast(getContext(), message, Toast.LENGTH_LONG, GB.ERROR); + } + fetchRecordedDataFinished(); + return raw.length == 5; + } + + boolean success = true; + try (DBHandler db = GBApplication.acquireDB()) { + GBDevice device = getDevice(); + DaoSession session = db.getDaoSession(); + + Long userId = DBHelper.getUser(session).getId(); + Long deviceId = DBHelper.getDevice(device, session).getId(); + + for (int record = 0; record < raw[2]; record++) { + success &= decodeRecording(raw, 3 + record * 32, device, session, deviceId, userId, record == 0); + } + } catch (Exception e) { + LOG.error("Error acquiring database for recording historic sample", e); + } + + if (FetchCurrent >= FetchTo || raw[2] < 7) { + fetchRecordedDataFinished(); + } + return success; + } + + private boolean decodeRecording(byte[] raw, int start, GBDevice device, DaoSession session, long deviceId, long userId, boolean updateProgress) { + if (raw.length < start + 32) { + LOG.error("length of history record is only from {} to {} instead of expected {}: {}", start, raw.length, start + 32, StringUtils.bytesToHex(raw)); + return false; + } + + int timestampPPG = BLETypeConversions.toUint32(raw, start); + int heartRate = BLETypeConversions.toUnsigned(raw[start + 4]); + int HRV = BLETypeConversions.toUnsigned(raw[start + 5]); + int spo2 = BLETypeConversions.toUnsigned(raw[start + 6]); + int recordType = BLETypeConversions.toUnsigned(raw[start + 7]); + + int timestampTemp = BLETypeConversions.toUint32(raw, start + 8); + float temperatureMax = Float.intBitsToFloat(BLETypeConversions.toUint32(raw, start + 12)); + float temperatureMin = Float.intBitsToFloat(BLETypeConversions.toUint32(raw, start + 16)); + + int timestampActivity = BLETypeConversions.toUint32(raw, start + 20); + int rawIntensity = BLETypeConversions.toUint16(raw[start + 24]); + int steps = BLETypeConversions.toUint16(raw[start + 26]); + int stress = (BLETypeConversions.toUint16(raw[start + 28]) * 100) / 255; + + int index = BLETypeConversions.toUint16(raw, start + 30); + + if (updateProgress) { + int target = (FetchTo - FetchFrom); + if (target != 0) { + int progress = ((index - FetchFrom) * 100) / target; + if (progress > 99) { + progress = 99; + } + GB.updateTransferNotification(null, Integer.toString(index), true, progress, getContext()); + } + } + + FetchCurrent = Integer.max(FetchCurrent, index); + + LOG.debug("record[{}]: timeA={}, heartRate={}, HRV={}, spo2={}, recordType={}, timestampTemp={}, tempMax={}, tempMin={}," + "timeC={}, rawIntensity={}, steps={}, stress={}", index, timestampPPG, heartRate, HRV, spo2, recordType, timestampTemp, temperatureMax, temperatureMin, timestampActivity, rawIntensity, steps, stress); + + if (heartRate != 0) { + GenericHeartRateSampleProvider provider = new GenericHeartRateSampleProvider(device, session); + GenericHeartRateSample sample = new GenericHeartRateSample(timestampPPG * 1000L, deviceId, userId, heartRate); + provider.addSample(sample); + } + + if (HRV != 0) { + GenericHrvValueSampleProvider provider = new GenericHrvValueSampleProvider(device, session); + GenericHrvValueSample sample = new GenericHrvValueSample(timestampPPG * 1000L, deviceId, userId, HRV); + provider.addSample(sample); + } + + if (spo2 != 0) { + GenericSpo2SampleProvider provider = new GenericSpo2SampleProvider(device, session); + GenericSpo2Sample sample = new GenericSpo2Sample(timestampPPG * 1000L, deviceId, userId, spo2); + provider.addSample(sample); + } + + if (temperatureMax != 0.0f || temperatureMin != 0.0f) { + float temperature = (temperatureMax + temperatureMin) / 2f; + GenericTemperatureSampleProvider provider = new GenericTemperatureSampleProvider(device, session); + GenericTemperatureSample sample = new GenericTemperatureSample(timestampTemp * 1000L, deviceId, userId, temperature, 0); + provider.addSample(sample); + } + + if (stress != 0) { + GenericStressSampleProvider provider = new GenericStressSampleProvider(device, session); + GenericStressSample sample = new GenericStressSample(timestampActivity * 1000L, deviceId, userId, stress); + provider.addSample(sample); + } + + if (rawIntensity != 0 || steps != 0 || heartRate != 0) { + int hr = (heartRate == 0) ? -1 : heartRate; + UltrahumanActivitySampleProvider provider = new UltrahumanActivitySampleProvider(device, session); + UltrahumanActivitySample sample = new UltrahumanActivitySample(timestampActivity, deviceId, userId, recordType, hr, rawIntensity, steps); + provider.addGBActivitySample(sample); + } + + return true; + } + + private boolean decodeDeviceState(byte[] raw) { + boolean success = false; + + BatteryState batteryState = BatteryState.UNKNOWN; + Integer batteryLevel = null; + Integer deviceState = null; + Integer deviceTemperature = null; + + try (DBHandler db = GBApplication.acquireDB()) { + if (raw.length != 7) { + LOG.warn("received Device State with unexpected length {}: {}", raw.length, StringUtils.bytesToHex(raw)); + } else { + batteryLevel = 0xFF & raw[0]; + // decoding of 1..4 is unknown + deviceState = 0xFF & raw[5]; + deviceTemperature = 0xFF & raw[6]; + + switch (deviceState) { + case 0x00: + batteryState = BatteryState.BATTERY_NORMAL; + break; + case 0x03: + batteryState = BatteryState.BATTERY_CHARGING; + break; + default: + LOG.warn("DeviceState contains unhandled device state {}: {}", raw[5], StringUtils.bytesToHex(raw)); + } + } + + GBDevice device = getDevice(); + DaoSession session = db.getDaoSession(); + + long now = System.currentTimeMillis(); + Long userId = DBHelper.getUser(session).getId(); + Long deviceId = DBHelper.getDevice(device, session).getId(); + + UltrahumanDeviceStateSample sample = new UltrahumanDeviceStateSample(now, deviceId, userId, raw, batteryLevel, deviceState, deviceTemperature); + + UltrahumanDeviceStateSampleProvider sampleProvider = new UltrahumanDeviceStateSampleProvider(device, session); + sampleProvider.addSample(sample); + success = true; + } catch (Exception e) { + LOG.error("Error acquiring database for recording device state sample", e); + LOG.warn("device state sample: {}", StringUtils.bytesToHex(raw)); + } + + GBDeviceEventBatteryInfo batteryEvent = new GBDeviceEventBatteryInfo(); + batteryEvent.level = (batteryLevel == null) ? -1 : batteryLevel; + batteryEvent.state = batteryState; + evaluateGBDeviceEvent(batteryEvent); + + return success; + } + + private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { + LOG.debug("handleDeviceInfo: {}", info); + GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + versionCmd.fwVersion = info.getFirmwareRevision(); + versionCmd.fwVersion2 = info.getSerialNumber(); + versionCmd.hwVersion = info.getHardwareRevision(); + handleGBDeviceEvent(versionCmd); + } + + private void fetchRecordedDataFinished() { + GB.updateTransferNotification(null, "", false, 100, getContext()); + LOG.info("Sync finished!"); + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + GB.signalActivityDataFinish(getDevice()); + } + + public void activateAirplaneMode() { + sendCommand("ActivateAirplaneMode", new byte[]{UltrahumanConstants.OPERATION_ACTIVATE_AIRPLANE_MODE}); + } + + @Override + public void onReset(int flags) { + if ((flags & GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) == GBDeviceProtocol.RESET_FLAGS_FACTORY_RESET) { + sendCommand("onReset", new byte[]{UltrahumanConstants.OPERATION_RESET}); + } + } + + @Override + public void onSetTime() { + TransactionBuilder builder = new TransactionBuilder("onSetTime"); + UltrahumanSetTimeAction action = new UltrahumanSetTimeAction(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND)); + builder.add(action); + + if (isConnected()) { + builder.queue(getQueue()); + } else { + GB.toast(getContext(), R.string.devicestatus_disconnected, Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void sendCommand(String taskName, byte[] contents) { + LOG.debug("sendCommand {} : {}", taskName, StringUtils.bytesToHex(contents)); + TransactionBuilder builder = new TransactionBuilder(taskName); + builder.write(getCharacteristic(UltrahumanConstants.UUID_CHARACTERISTIC_COMMAND), contents); + + if (isConnected()) { + builder.queue(getQueue()); + } else { + GB.toast(getContext(), R.string.devicestatus_disconnected, Toast.LENGTH_LONG, GB.ERROR); + } + } + + private class UltrahumanBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + final GBDevice device = intent.getParcelableExtra(GBDevice.EXTRA_DEVICE); + if (device != null && !device.equals(getDevice())) { + // this intent is for another device + return; + } + + String action = intent.getAction(); + if (UltrahumanConstants.ACTION_AIRPLANE_MODE.equals(action)) { + activateAirplaneMode(); + } + } + } + + private class UltrahumanIntentListener implements IntentListener { + @Override + public void notify(Intent intent) { + String action = intent.getAction(); + if (DeviceInfoProfile.ACTION_DEVICE_INFO.equals(action)) { + handleDeviceInfo(intent.getParcelableExtra(DeviceInfoProfile.EXTRA_DEVICE_INFO)); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/ultrahuman/UltrahumanSetTimeAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/ultrahuman/UltrahumanSetTimeAction.java new file mode 100644 index 0000000000..8a64e66fe8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/ultrahuman/UltrahumanSetTimeAction.java @@ -0,0 +1,50 @@ +/* Copyright (C) 2025 Thomas Kuehne + + 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.service.devices.ultrahuman; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; + +import java.util.Calendar; + +import nodomain.freeyourgadget.gadgetbridge.devices.ultrahuman.UltrahumanConstants; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.WriteAction; +import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; + +/// calculate date/time on the fly to avoid setting an outdated value +class UltrahumanSetTimeAction extends WriteAction { + UltrahumanSetTimeAction(BluetoothGattCharacteristic characteristic) { + super(characteristic, null); + } + + @Override + protected boolean writeValue(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { + final Calendar calendar = DateTimeUtils.getCalendarUTC(); + final long millis = calendar.getTimeInMillis(); + final long epoc = Math.round(millis / 1000.0d); + + byte[] command = new byte[]{ + UltrahumanConstants.OPERATION_SETTIME, + (byte) (epoc & 0xff), + (byte) ((epoc >> 8) & 0xff), + (byte) ((epoc >> 16) & 0xff), + (byte) ((epoc >> 24) & 0xff) + }; + return super.writeValue(gatt, characteristic, command); + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ffb83d1aa..19862fe2b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1964,6 +1964,7 @@ Colmi R06 Colmi R09 Colmi R10 + Ultrahuman Ring AIR Bowers and Wilkins P series EarFun Air S EarFun Air Pro 4 @@ -3738,4 +3739,11 @@ Noise Control (Normal, Ambient Sound) Noise Control (Wind Noise, Ambient Sound) Redial + airplane mode activated + ring is charging - airplane mode NOT activated + ring is fully charged - airplane mode NOT activated + unknown error - airplane mode NOT activated + Activate airplane mode? + airplane mode + received unhandled response code 0x%x for operation 0x%x