mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Add experimental support for Ultrahuman Air smart ring (#4602)
This adds basic support for the Ultrahuman Air smart ring: https://www.ultrahuman.com/ (#4573) Working: - [x] pairing - [x] fetching historic records - [X] charting HR - [x] charting blood oxygen - [x] charting temperature - [x] charting activity - [x] charting steps - [x] charting stress - [x] activating air plane mode Protocol documentation can be found in Freeyourgadget/website#86 HRV is fetched but not charted as the device has no HrvSummarySampleProvider. Co-authored-by: Thomas Kuehne <thomas.kuehne@gmx.li> Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/4602 Co-authored-by: ThomasKuehne <thomaskuehne@noreply.codeberg.org> Co-committed-by: ThomasKuehne <thomaskuehne@noreply.codeberg.org>
This commit is contained in:
committed by
José Rebelo
co-authored by
Thomas Kuehne
parent
329514d04d
commit
a1cdfe8c89
+86
-13
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<GenericHeartRateSample> {
|
||||
public GenericHeartRateSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GenericHeartRateSample, ?> 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();
|
||||
}
|
||||
}
|
||||
+56
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<GenericHrvValueSample> {
|
||||
public GenericHrvValueSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GenericHrvValueSample, ?> 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();
|
||||
}
|
||||
}
|
||||
+57
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<GenericSpo2Sample> {
|
||||
public GenericSpo2SampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GenericSpo2Sample, ?> 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();
|
||||
}
|
||||
}
|
||||
+56
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<GenericStressSample> {
|
||||
public GenericStressSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GenericStressSample, ?> 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();
|
||||
}
|
||||
}
|
||||
+56
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<GenericTemperatureSample> {
|
||||
public GenericTemperatureSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<GenericTemperatureSample, ?> 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();
|
||||
}
|
||||
}
|
||||
+39
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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;
|
||||
}
|
||||
+67
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
+229
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<DeviceCardAction> 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<AbstractDao<?, ?>, Property> daoMap = new HashMap<AbstractDao<?, ?>, 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<AbstractDao<?, ?>, 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<? extends DeviceSupport> getDeviceSupportClass() {
|
||||
return UltrahumanDeviceSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Ultrahuman Healthcare Pvt Ltd";
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends HrvValueSample> getHrvValueSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new GenericHrvValueSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends HeartRateSample> getHeartRateMaxSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new GenericHeartRateSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(final GBDevice device, final DaoSession session) {
|
||||
return new UltrahumanActivitySampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
|
||||
return new GenericSpo2SampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimeSampleProvider<? extends StressSample> 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<? extends TemperatureSample> 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;
|
||||
}
|
||||
}
|
||||
+106
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<UltrahumanActivitySample> {
|
||||
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<UltrahumanActivitySample, ?> 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();
|
||||
}
|
||||
}
|
||||
+57
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<UltrahumanDeviceStateSample> {
|
||||
public UltrahumanDeviceStateSampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AbstractDao<UltrahumanDeviceStateSample, ?> 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();
|
||||
}
|
||||
}
|
||||
+43
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
+552
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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<UltrahumanDeviceSupport> 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1964,6 +1964,7 @@
|
||||
<string name="devicetype_colmi_r06">Colmi R06</string>
|
||||
<string name="devicetype_colmi_r09">Colmi R09</string>
|
||||
<string name="devicetype_colmi_r10">Colmi R10</string>
|
||||
<string name="devicetype_ultrahuma_ring_air">Ultrahuman Ring AIR</string>
|
||||
<string name="devicetype_bandw_pseries">Bowers and Wilkins P series</string>
|
||||
<string name="devicetype_earfun_air_s">EarFun Air S</string>
|
||||
<string name="devicetype_earfun_air_pro_4">EarFun Air Pro 4</string>
|
||||
@@ -3738,4 +3739,11 @@
|
||||
<string name="earfun_tap_action_noise_control_normal_ambient">Noise Control (Normal, Ambient Sound)</string>
|
||||
<string name="earfun_tap_action_noise_control_wind_ambient">Noise Control (Wind Noise, Ambient Sound)</string>
|
||||
<string name="earfun_tap_action_redial">Redial</string>
|
||||
<string name="ultrahuman_airplane_mode_activated">airplane mode activated</string>
|
||||
<string name="ultrahuman_airplane_mode_on_charger">ring is charging - airplane mode NOT activated</string>
|
||||
<string name="ultrahuman_airplane_mode_too_full">ring is fully charged - airplane mode NOT activated</string>
|
||||
<string name="ultrahuman_airplane_mode_unknown">unknown error - airplane mode NOT activated</string>
|
||||
<string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string>
|
||||
<string name="ultrahuman_airplane_mode_title">airplane mode</string>
|
||||
<string name="ultrahuman_unhandled_error_response">received unhandled response code 0x%x for operation 0x%x</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user