Huawei: retrieve SpO2 data during workout. No UI.

This commit is contained in:
Me7c7 2025-01-27 16:28:14 +02:00 committed by José Rebelo
parent 50fda01a56
commit cfceada4c5
10 changed files with 302 additions and 35 deletions

View File

@ -54,7 +54,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception {
final Schema schema = new Schema(94, MAIN_PACKAGE + ".entities");
final Schema schema = new Schema(95, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes);
@ -160,6 +160,7 @@ public class GBDaoGenerator {
addHuaweiWorkoutDataSample(schema, huaweiWorkoutSummary);
addHuaweiWorkoutPaceSample(schema, huaweiWorkoutSummary);
addHuaweiWorkoutSwimSegmentsSample(schema, huaweiWorkoutSummary);
addHuaweiWorkoutSpO2Sample(schema, huaweiWorkoutSummary);
Entity huaweiDictData = addHuaweiDictData(schema, user, device);
addHuaweiDictDataValues(schema, huaweiDictData);
@ -1544,6 +1545,20 @@ public class GBDaoGenerator {
return workoutSwimSegmentsSample;
}
private static Entity addHuaweiWorkoutSpO2Sample(Schema schema, Entity summaryEntity) {
Entity workoutSwimSegmentsSample = addEntity(schema, "HuaweiWorkoutSpO2Sample");
workoutSwimSegmentsSample.setJavaDoc("Contains Huawei Workout SpO2 data samples");
Property id = workoutSwimSegmentsSample.addLongProperty("workoutId").primaryKey().notNull().getProperty();
workoutSwimSegmentsSample.addToOne(summaryEntity, id);
workoutSwimSegmentsSample.addIntProperty("interval").notNull().primaryKey();
workoutSwimSegmentsSample.addIntProperty("value").notNull();
return workoutSwimSegmentsSample;
}
private static Entity addHuaweiDictData(Schema schema, Entity user, Entity device) {
Entity dictData = addEntity(schema, "HuaweiDictData");

View File

@ -51,6 +51,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiDictDataValuesDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSampleDao;
@ -135,6 +136,10 @@ public class HuaweiCoordinator {
session.getHuaweiWorkoutSwimSegmentsSampleDao().queryBuilder().where(
HuaweiWorkoutSwimSegmentsSampleDao.Properties.WorkoutId.eq(sample.getWorkoutId())
).buildDelete().executeDeleteWithoutDetachingEntities();
session.getHuaweiWorkoutSpO2SampleDao().queryBuilder().where(
HuaweiWorkoutSpO2SampleDao.Properties.WorkoutId.eq(sample.getWorkoutId())
).buildDelete().executeDeleteWithoutDetachingEntities();
}
session.getHuaweiWorkoutSummarySampleDao().queryBuilder().where(HuaweiWorkoutSummarySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();

View File

@ -580,6 +580,8 @@ public class HuaweiPacket {
return new Workout.WorkoutPace.Response(paramsProvider).fromPacket(this);
case Workout.WorkoutSwimSegments.id:
return new Workout.WorkoutSwimSegments.Response(paramsProvider).fromPacket(this);
case Workout.WorkoutSpO2.id:
return new Workout.WorkoutSpO2.Response(paramsProvider).fromPacket(this);
default:
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;

View File

@ -16,6 +16,8 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@ -60,6 +62,7 @@ public class Workout {
public short dataCount;
public short paceCount;
public short segmentsCount = 0;
public short spO2Count = 0;
}
@ -90,6 +93,9 @@ public class Workout {
if(subContainerTlv.contains(0x09)) {
workoutNumber.segmentsCount = subContainerTlv.getShort(0x09);
}
if(subContainerTlv.contains(0x0c)) {
workoutNumber.spO2Count = subContainerTlv.getShort(0x0c);
}
this.workoutNumbers.add(workoutNumber);
}
@ -720,6 +726,77 @@ public class Workout {
}
}
public static class WorkoutSpO2 {
public static final int id = 0x14;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
short workoutNumber,
short spO2Number
) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, workoutNumber)
.put(0x02, spO2Number)
.put(0x03);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public static class Block {
public int interval = -1;
public int value = -1;
@NonNull
@Override
public String toString() {
final StringBuffer sb = new StringBuffer("Block{");
sb.append("interval=").append(interval);
sb.append(", value=").append(value);
sb.append('}');
return sb.toString();
}
}
public short spO2Number1; //TODO: meaning of this field
public short spO2Number2; //TODO: meaning of this field
public List<Block> blocks;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
this.spO2Number1 = this.tlv.getShort(0x01);
this.spO2Number2 = this.tlv.getShort(0x02);
HuaweiTLV container = this.tlv.getObject(0x83);
this.blocks = new ArrayList<>();
for (HuaweiTLV blockTlv : container.getObjects(0x84)) {
Block block = new Block();
if (blockTlv.contains(0x05))
block.interval = blockTlv.getAsInteger(0x05);
if (blockTlv.contains(0x06))
block.value = blockTlv.getAsInteger(0x06);
blocks.add(block);
}
}
}
}
public static class NotifyHeartRate {
public static final int id = 0x17;

View File

@ -80,6 +80,8 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSample;
@ -1899,6 +1901,33 @@ public class HuaweiSupportProvider {
}
}
public void addWorkoutSpO2Data(Long workoutId, List<Workout.WorkoutSpO2.Response.Block> spO2List, short number) {
if (workoutId == null)
return;
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSpO2SampleDao dao = db.getDaoSession().getHuaweiWorkoutSpO2SampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSpO2Sample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSpO2SampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
for (Workout.WorkoutSpO2.Response.Block block : spO2List) {
HuaweiWorkoutSpO2Sample spO2Sample = new HuaweiWorkoutSpO2Sample(
workoutId,
block.interval,
block.value
);
dao.insertOrReplace(spO2Sample);
}
} catch (Exception e) {
LOG.error("Failed to add workout SpO2 data to database", e);
}
}
public void addDictData(List<HuaweiP2PDataDictionarySyncService.DictData> dictData) {
try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId();

View File

@ -38,6 +38,7 @@ public class GetWorkoutDataRequest extends Request {
/**
* Request to get workout totals
*
* @param support The support
* @param workoutNumbers The numbers of the current workout
* @param remainder The numbers of the remainder if the workouts to get
@ -79,13 +80,13 @@ public class GetWorkoutDataRequest extends Request {
throw new WorkoutParseException("Incorrect data number!");
LOG.info("Workout {} data {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("Workout : " + packet.workoutNumber);
LOG.info("Data num: " + packet.dataNumber);
LOG.info("Header : " + Arrays.toString(packet.rawHeader));
LOG.info("Header : " + packet.header);
LOG.info("Data : " + Arrays.toString(packet.rawData));
LOG.info("Data : " + Arrays.toString(packet.dataList.toArray()));
LOG.info("Bitmap : " + packet.innerBitmap);
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Data num: {}", packet.dataNumber);
LOG.info("Header : {}", Arrays.toString(packet.rawHeader));
LOG.info("Header : {}", packet.header);
LOG.info("Data : {}", Arrays.toString(packet.rawData));
LOG.info("Data : {}", Arrays.toString(packet.dataList.toArray()));
LOG.info("Bitmap : {}", packet.innerBitmap);
this.supportProvider.addWorkoutSampleData(
this.databaseId,
@ -122,6 +123,16 @@ public class GetWorkoutDataRequest extends Request {
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {

View File

@ -72,10 +72,10 @@ public class GetWorkoutPaceRequest extends Request {
throw new WorkoutParseException("Incorrect pace number!");
LOG.info("Workout {} pace {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("Workout : " + packet.workoutNumber);
LOG.info("Pace : " + packet.paceNumber);
LOG.info("Block num: " + packet.blocks.size());
LOG.info("Blocks : " + Arrays.toString(packet.blocks.toArray()));
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Pace : {}", packet.paceNumber);
LOG.info("Block num: {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutPaceData(this.databaseId, packet.blocks, packet.paceNumber);
@ -99,6 +99,16 @@ public class GetWorkoutPaceRequest extends Request {
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {

View File

@ -0,0 +1,97 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
public class GetWorkoutSpO2Request extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutSpO2Request.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
short number;
Long databaseId;
public GetWorkoutSpO2Request(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder, short number, Long databaseId) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutSpO2.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
this.number = number;
this.databaseId = databaseId;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutSpO2.Request(paramsProvider, this.workoutNumbers.workoutNumber, this.number).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutSpO2.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutSwimSegments.Response.class);
Workout.WorkoutSpO2.Response packet = (Workout.WorkoutSpO2.Response) receivedPacket;
LOG.info("Workout {} current {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("spO2Number1: {}", packet.spO2Number1);
LOG.info("spO2Number2: {}", packet.spO2Number2);
LOG.info("Block num : {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutSpO2Data(this.databaseId, packet.blocks, this.number);
if (this.workoutNumbers.spO2Count > this.number + 1) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) (this.number + 1),
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {
@Override
public void run() {
if (!remainder.isEmpty()) {
GetWorkoutTotalsRequest nextRequest = new GetWorkoutTotalsRequest(
GetWorkoutSpO2Request.this.supportProvider,
remainder.remove(0),
remainder
);
nextRequest.setFinalizeReq(GetWorkoutSpO2Request.this.finalizeReq);
// Cannot do this with nextRequest because it's in a callback
try {
nextRequest.doPerform();
} catch (IOException e) {
finalizeReq.handleException(new ResponseParseException("Cannot send next request", e));
}
} else {
supportProvider.endOfWorkoutSync();
}
}
});
}
}
}

View File

@ -56,10 +56,10 @@ public class GetWorkoutSwimSegmentsRequest extends Request {
throw new WorkoutParseException("Incorrect pace number!");
LOG.info("Workout {} segment {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("Workout : " + packet.workoutNumber);
LOG.info("Segments : " + packet.segmentNumber);
LOG.info("Block num: " + packet.blocks.size());
LOG.info("Blocks : " + Arrays.toString(packet.blocks.toArray()));
LOG.info("Workout : {}", packet.workoutNumber);
LOG.info("Segments : {}", packet.segmentNumber);
LOG.info("Block num: {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutSwimSegmentsData(this.databaseId, packet.blocks, packet.segmentNumber);
@ -73,6 +73,16 @@ public class GetWorkoutSwimSegmentsRequest extends Request {
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {

View File

@ -35,6 +35,7 @@ public class GetWorkoutTotalsRequest extends Request {
/**
* Request to get workout totals
*
* @param support The support
* @param workoutNumbers The numbers of the current workout
* @param remainder The numbers of the remainder of the workouts to get
@ -69,16 +70,16 @@ public class GetWorkoutTotalsRequest extends Request {
throw new WorkoutParseException("Incorrect workout number!");
LOG.info("Workout {} totals:", this.workoutNumbers.workoutNumber);
LOG.info("Number : " + packet.number);
LOG.info("Status : " + packet.status);
LOG.info("Start : " + packet.startTime);
LOG.info("End : " + packet.endTime);
LOG.info("Calories: " + packet.calories);
LOG.info("Distance: " + packet.distance);
LOG.info("Steps : " + packet.stepCount);
LOG.info("Time : " + packet.totalTime);
LOG.info("Duration: " + packet.duration);
LOG.info("Type : " + packet.type);
LOG.info("Number : {}", packet.number);
LOG.info("Status : {}", packet.status);
LOG.info("Start : {}", packet.startTime);
LOG.info("End : {}", packet.endTime);
LOG.info("Calories: {}", packet.calories);
LOG.info("Distance: {}", packet.distance);
LOG.info("Steps : {}", packet.stepCount);
LOG.info("Time : {}", packet.totalTime);
LOG.info("Duration: {}", packet.duration);
LOG.info("Type : {}", packet.type);
Long databaseId = this.supportProvider.addWorkoutTotalsData(packet);
@ -113,6 +114,16 @@ public class GetWorkoutTotalsRequest extends Request {
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else if (this.workoutNumbers.spO2Count > 0) {
GetWorkoutSpO2Request nextRequest = new GetWorkoutSpO2Request(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, databaseId, new Runnable() {