Fossil/Skagen Gen. 6 Hybrids: Add SpO2 Support

This commit is contained in:
Benjamin Swartley 2024-11-30 15:45:34 -05:00
parent b3f29d16b3
commit 4f30648886
6 changed files with 114 additions and 15 deletions

View File

@ -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);

View File

@ -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 <https://www.gnu.org/licenses/>. */
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<HybridHRSpo2Sample> {
public HybridHRSpo2SampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@NonNull
@Override
public AbstractDao<HybridHRSpo2Sample, ?> 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();
}
}

View File

@ -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");
}
}

View File

@ -550,7 +550,7 @@ public class FossilWatchAdapter extends WatchAdapter {
public void handleFileData(byte[] fileData) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
ActivityFileParser parser = new ActivityFileParser();
ArrayList<ActivityEntry> entries = parser.parseFile(fileData);
ArrayList<ActivityEntry> entries = parser.parseFile(fileData).getKey();
HybridHRActivitySampleProvider provider = new HybridHRActivitySampleProvider(getDeviceSupport().getDevice(), dbHandler.getDaoSession());
HybridHRActivitySample[] samples = new HybridHRActivitySample[entries.size()];

View File

@ -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<ActivityEntry> entries = parser.parseFile(fileData);
Map.Entry<ArrayList<ActivityEntry>, ArrayList<HybridHRSpo2Sample>> parsedEntries = parser.parseFile(fileData);
// Activities
ArrayList<ActivityEntry> 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<HybridHRSpo2Sample> 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);

View File

@ -15,10 +15,12 @@
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.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<ActivityEntry> parseFile(byte[] file) {
public Map.Entry<ArrayList<ActivityEntry>, ArrayList<HybridHRSpo2Sample>> 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<ActivityEntry> samples = new ArrayList<>();
ArrayList<HybridHRSpo2Sample> 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) {