diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
index f5c81147b..8f18dfbb3 100644
--- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
+++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java
@@ -54,7 +54,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
- final Schema schema = new Schema(89, MAIN_PACKAGE + ".entities");
+ final Schema schema = new Schema(90, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@@ -115,6 +115,7 @@ public class GBDaoGenerator {
addPineTimeActivitySample(schema, user, device);
addWithingsSteelHRActivitySample(schema, user, device);
addHybridHRActivitySample(schema, user, device);
+ addHybridHRSpo2Sample(schema, user, device);
addVivomoveHrActivitySample(schema, user, device);
addGarminFitFile(schema, user, device);
addGarminActivitySample(schema, user, device);
@@ -734,6 +735,13 @@ public class GBDaoGenerator {
return activitySample;
}
+ 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);
+ return spo2sample;
+ }
+
private static Entity addCyclingSample(Schema schema, Entity user, Entity device) {
Entity cyclingSample = addEntity(schema, "CyclingSample");
addCommonTimeSampleProperties("AbstractTimeSample", cyclingSample, user, device);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRSpo2SampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRSpo2SampleProvider.java
new file mode 100644
index 000000000..3bad94b2b
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/HybridHRSpo2SampleProvider.java
@@ -0,0 +1,56 @@
+/* Copyright (C) 2024 Benjamin Swartley
+
+ 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.qhybrid;
+
+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.HybridHRSpo2Sample;
+import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRSpo2SampleDao;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+
+public class HybridHRSpo2SampleProvider extends AbstractTimeSampleProvider {
+ public HybridHRSpo2SampleProvider(GBDevice device, DaoSession session) {
+ super(device, session);
+ }
+
+ @NonNull
+ @Override
+ public AbstractDao getSampleDao() {
+ return getSession().getHybridHRSpo2SampleDao();
+ }
+
+ @NonNull
+ @Override
+ protected Property getTimestampSampleProperty() {
+ return HybridHRSpo2SampleDao.Properties.Timestamp;
+ }
+
+ @NonNull
+ @Override
+ protected Property getDeviceIdentifierSampleProperty() {
+ return HybridHRSpo2SampleDao.Properties.DeviceId;
+ }
+
+ @Override
+ public HybridHRSpo2Sample createSample() {
+ return new HybridHRSpo2Sample();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java
index cba18d037..35bc7288b 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/qhybrid/QHybridCoordinator.java
@@ -45,11 +45,13 @@ import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpec
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.TimeSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
@@ -103,6 +105,11 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
return new HybridHRActivitySampleProvider(device, session);
}
+ @Override
+ public TimeSampleProvider extends Spo2Sample> getSpo2SampleProvider(GBDevice device, DaoSession session) {
+ return new HybridHRSpo2SampleProvider(device, session);
+ }
+
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
if (isHybridHR()) {
@@ -338,4 +345,9 @@ public class QHybridCoordinator extends AbstractBLEDeviceCoordinator {
public boolean supportsNavigation() {
return isHybridHR();
}
+
+ @Override
+ public boolean supportsSpo2(GBDevice device) {
+ return device.getName().equals("Fossil Gen. 6 Hybrid");
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java
index 91d5a8783..824d5bd55 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil/FossilWatchAdapter.java
@@ -550,7 +550,7 @@ public class FossilWatchAdapter extends WatchAdapter {
public void handleFileData(byte[] fileData) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
ActivityFileParser parser = new ActivityFileParser();
- ArrayList entries = parser.parseFile(fileData);
+ ArrayList entries = parser.parseFile(fileData).getKey();
HybridHRActivitySampleProvider provider = new HybridHRActivitySampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()];
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java
index 5a043de98..46125d7ca 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/adapter/fossil_hr/FossilHRWatchAdapter.java
@@ -102,8 +102,12 @@ import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.CommuteActionsActivi
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilFileReader;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.FossilHRInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRSpo2SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
+import nodomain.freeyourgadget.gadgetbridge.entities.Device;
+import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRSpo2Sample;
import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceApp;
@@ -1232,19 +1236,28 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
@Override
public void handleFileData(byte[] fileData) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
+ User user = DBHelper.getUser(dbHandler.getDaoSession());
+ Long userId = user.getId();
+ Device device = DBHelper.getDevice(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
+ Long deviceId = device.getId();
ActivityFileParser parser = new ActivityFileParser();
- ArrayList entries = parser.parseFile(fileData);
+ Map.Entry, ArrayList> parsedEntries = parser.parseFile(fileData);
+ // Activities
+ ArrayList entries = parsedEntries.getKey();
HybridHRActivitySampleProvider provider = new HybridHRActivitySampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
-
HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()];
-
- Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
- Long deviceId = DBHelper.getDevice(getDeviceSupport().getDevice(), dbHandler.getDaoSession()).getId();
for (int i = 0; i < entries.size(); i++) {
samples[i] = entries.get(i).toDAOActivitySample(userId, deviceId);
}
-
provider.addGBActivitySamples(samples);
+ // SpO2, should be empty for an unsupported device
+ ArrayList spo2Samples = parsedEntries.getValue();
+ HybridHRSpo2SampleProvider spo2Provider = new HybridHRSpo2SampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
+ for (HybridHRSpo2Sample sample : spo2Samples) {
+ sample.setDevice(device);
+ sample.setUser(user);
+ }
+ spo2Provider.addSamples(spo2Samples);
if (saveRawActivityFiles) {
writeFile(String.valueOf(System.currentTimeMillis()), fileData);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityFileParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityFileParser.java
index b79f4a01b..51356e76f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityFileParser.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/qhybrid/parser/ActivityFileParser.java
@@ -15,10 +15,12 @@
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.qhybrid.parser;
+import nodomain.freeyourgadget.gadgetbridge.entities.HybridHRSpo2Sample;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
+import java.util.Map;
public class ActivityFileParser {
@@ -28,10 +30,9 @@ public class ActivityFileParser {
int currentTimestamp = 0; // Aligns with `e2 04` from my testing
ActivityEntry currentSample = null;
int currentId = 1;
- int spO2 = -1; // Should actually do something with this
- public ArrayList parseFile(byte[] file) {
+ public Map.Entry, ArrayList> parseFile(byte[] file) {
ByteBuffer buffer = ByteBuffer.wrap(file);
buffer.order(ByteOrder.LITTLE_ENDIAN);
@@ -48,6 +49,7 @@ public class ActivityFileParser {
buffer.position(52); // Seem to be another 32 bytes after the initial 20 stop
ArrayList samples = new ArrayList<>();
+ ArrayList spo2Samples = new ArrayList<>();
finishCurrentPacket(samples);
@@ -97,7 +99,12 @@ public class ActivityFileParser {
continue; // Not sure what to do with this
} else if (f1 == (byte) 0xD6) {
- buffer.get(new byte[4]);
+ HybridHRSpo2Sample spo2Sample = new HybridHRSpo2Sample();
+ spo2Sample.setTimestamp(currentTimestamp * 1000L);
+ spo2Sample.setSpo2(buffer.get() & 0xFF);
+ spo2Samples.add(spo2Sample);
+
+ buffer.get(new byte[3]); // Likely something to do with sample statistics
} else if (f1 == (byte) 0xFE && f2 == (byte) 0xFE) {
if (buffer.get(buffer.position()) == (byte) 0xFE) { buffer.get(); } // WHY?
@@ -163,9 +170,12 @@ public class ActivityFileParser {
break;
- case (byte) 0xD6: // Seems to only come from intentional spot-checks, despite watch's value updating independently on occasion.
- spO2 = buffer.get() & 0xFF;
-
+ case (byte) 0xD6:
+ buffer.get(); // Likely some statistic, notably different from 0xCE 0xD6
+ HybridHRSpo2Sample spo2Sample = new HybridHRSpo2Sample();
+ spo2Sample.setTimestamp(currentTimestamp * 1000L);
+ spo2Sample.setSpo2(buffer.get() & 0xFF);
+ spo2Samples.add(spo2Sample);
break;
case (byte) 0xCB: // Very rare, may even be removed
@@ -181,7 +191,7 @@ public class ActivityFileParser {
}
- return samples;
+ return Map.entry(samples, spo2Samples);
}
private static boolean elemValidFlags(byte value) {