Huawei: Parse more workout information. No UI.

This commit is contained in:
Me7c7 2025-01-31 20:04:33 +02:00
parent 02aa267e8f
commit fdea3b7955
14 changed files with 692 additions and 40 deletions

View File

@ -54,7 +54,7 @@ public class GBDaoGenerator {
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
final Schema schema = new Schema(95, MAIN_PACKAGE + ".entities"); final Schema schema = new Schema(96, MAIN_PACKAGE + ".entities");
Entity userAttributes = addUserAttributes(schema); Entity userAttributes = addUserAttributes(schema);
Entity user = addUserInfo(schema, userAttributes); Entity user = addUserInfo(schema, userAttributes);
@ -161,6 +161,7 @@ public class GBDaoGenerator {
addHuaweiWorkoutPaceSample(schema, huaweiWorkoutSummary); addHuaweiWorkoutPaceSample(schema, huaweiWorkoutSummary);
addHuaweiWorkoutSwimSegmentsSample(schema, huaweiWorkoutSummary); addHuaweiWorkoutSwimSegmentsSample(schema, huaweiWorkoutSummary);
addHuaweiWorkoutSpO2Sample(schema, huaweiWorkoutSummary); addHuaweiWorkoutSpO2Sample(schema, huaweiWorkoutSummary);
addHuaweiWorkoutSectionsSample(schema, huaweiWorkoutSummary);
Entity huaweiDictData = addHuaweiDictData(schema, user, device); Entity huaweiDictData = addHuaweiDictData(schema, user, device);
addHuaweiDictDataValues(schema, huaweiDictData); addHuaweiDictDataValues(schema, huaweiDictData);
@ -1502,6 +1503,18 @@ public class GBDaoGenerator {
workoutDataSample.addShortProperty("frequency").notNull(); workoutDataSample.addShortProperty("frequency").notNull();
workoutDataSample.addIntProperty("altitude"); workoutDataSample.addIntProperty("altitude");
workoutDataSample.addShortProperty("hangTime").notNull();
workoutDataSample.addShortProperty("impactHangRate").notNull();
workoutDataSample.addByteProperty("rideCadence").notNull();
workoutDataSample.addFloatProperty("ap").notNull();
workoutDataSample.addFloatProperty("vo").notNull();
workoutDataSample.addFloatProperty("gtb").notNull();
workoutDataSample.addFloatProperty("vr").notNull();
workoutDataSample.addByteProperty("ceiling").notNull();
workoutDataSample.addByteProperty("temp").notNull();
workoutDataSample.addByteProperty("spo2").notNull();
workoutDataSample.addShortProperty("cns").notNull();
return workoutDataSample; return workoutDataSample;
} }
@ -1559,6 +1572,39 @@ public class GBDaoGenerator {
return workoutSwimSegmentsSample; return workoutSwimSegmentsSample;
} }
private static Entity addHuaweiWorkoutSectionsSample(Schema schema, Entity summaryEntity) {
Entity workoutSectionsSample = addEntity(schema, "HuaweiWorkoutSectionsSample");
workoutSectionsSample.setJavaDoc("Contains Huawei Workout Section data samples");
Property id = workoutSectionsSample.addLongProperty("workoutId").primaryKey().notNull().getProperty();
workoutSectionsSample.addToOne(summaryEntity, id);
workoutSectionsSample.addIntProperty("dataIdx").notNull().primaryKey();
workoutSectionsSample.addIntProperty("rowIdx").notNull().primaryKey();
workoutSectionsSample.addIntProperty("num").notNull();
workoutSectionsSample.addLongProperty("time").notNull();
workoutSectionsSample.addLongProperty("distance").notNull();
workoutSectionsSample.addIntProperty("pace").notNull();
workoutSectionsSample.addIntProperty("heartRate").notNull();
workoutSectionsSample.addIntProperty("cadence").notNull();
workoutSectionsSample.addIntProperty("stepLength").notNull();
workoutSectionsSample.addLongProperty("totalRise").notNull();
workoutSectionsSample.addLongProperty("totalDescend").notNull();
workoutSectionsSample.addIntProperty("groundContactTime").notNull();
workoutSectionsSample.addIntProperty("groundImpact").notNull();
workoutSectionsSample.addIntProperty("swingAngle").notNull();
workoutSectionsSample.addIntProperty("eversion").notNull();
workoutSectionsSample.addIntProperty("avgCadence").notNull();
workoutSectionsSample.addIntProperty("intervalTrainingType").notNull();
workoutSectionsSample.addIntProperty("divingMaxDepth").notNull();
workoutSectionsSample.addIntProperty("divingUnderwaterTime").notNull();
workoutSectionsSample.addIntProperty("divingBreakTime").notNull();
return workoutSectionsSample;
}
private static Entity addHuaweiDictData(Schema schema, Entity user, Entity device) { private static Entity addHuaweiDictData(Schema schema, Entity user, Entity device) {
Entity dictData = addEntity(schema, "HuaweiDictData"); Entity dictData = addEntity(schema, "HuaweiDictData");

View File

@ -0,0 +1,74 @@
package nodomain.freeyourgadget.gadgetbridge.database.schema;
import android.database.sqlite.SQLiteDatabase;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.database.DBUpdateScript;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
public class GadgetbridgeUpdate_96 implements DBUpdateScript {
@Override
public void upgradeSchema(final SQLiteDatabase db) {
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.HangTime.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.HangTime.columnName + "\" INTEGER NOT NULL DEFAULT -1;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.ImpactHangRate.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.ImpactHangRate.columnName + "\" INTEGER NOT NULL DEFAULT -1;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.RideCadence.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.RideCadence.columnName + "\" INTEGER NOT NULL DEFAULT -1;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Ap.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Ap.columnName + "\" FLOAT NOT NULL DEFAULT 0;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Vo.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Vo.columnName + "\" FLOAT NOT NULL DEFAULT 0;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Gtb.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Gtb.columnName + "\" FLOAT NOT NULL DEFAULT 0;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Vr.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Vr.columnName + "\" FLOAT NOT NULL DEFAULT 0;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Ceiling.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Ceiling.columnName + "\" INTEGER NOT NULL DEFAULT -1;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Temp.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Temp.columnName + "\" INTEGER NOT NULL DEFAULT -1;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Spo2.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Spo2.columnName + "\" INTEGER NOT NULL DEFAULT -1;";
db.execSQL(statement);
}
if (!DBHelper.existsColumn(HuaweiWorkoutDataSampleDao.TABLENAME, HuaweiWorkoutDataSampleDao.Properties.Cns.columnName, db)) {
final String statement = "ALTER TABLE " + HuaweiWorkoutDataSampleDao.TABLENAME + " ADD COLUMN \""
+ HuaweiWorkoutDataSampleDao.Properties.Cns.columnName + "\" INTEGER NOT NULL DEFAULT -1;";
db.execSQL(statement);
}
}
@Override
public void downgradeSchema(final SQLiteDatabase db) {
}
}

View File

@ -75,6 +75,8 @@ public class HuaweiCoordinator {
private boolean supportsTruSleepNewSync = false; private boolean supportsTruSleepNewSync = false;
private boolean supportsGpsNewSync = false; private boolean supportsGpsNewSync = false;
private boolean supportsWorkoutNewSteps = false;
private Watchface.WatchfaceDeviceParams watchfaceDeviceParams; private Watchface.WatchfaceDeviceParams watchfaceDeviceParams;
private App.AppDeviceParams appDeviceParams; private App.AppDeviceParams appDeviceParams;
@ -585,6 +587,10 @@ public class HuaweiCoordinator {
return supportsCommandForService(0x17, 0x01); return supportsCommandForService(0x17, 0x01);
} }
public boolean supportsWorkoutCapability() {
return supportsCommandForService(0x17, 0x15);
}
public boolean supportsWorkoutsTrustHeartRate() { public boolean supportsWorkoutsTrustHeartRate() {
return supportsCommandForService(0x17, 0x17); return supportsCommandForService(0x17, 0x17);
} }
@ -966,6 +972,14 @@ public class HuaweiCoordinator {
this.supportsGpsNewSync = supportsGpsNewSync; this.supportsGpsNewSync = supportsGpsNewSync;
} }
public boolean isSupportsWorkoutNewSteps() {
return supportsWorkoutNewSteps;
}
public void setSupportsWorkoutNewSteps(boolean supportsWorkoutNewSteps) {
this.supportsWorkoutNewSteps = supportsWorkoutNewSteps;
}
public String getOtaSoftwareVersion() { public String getOtaSoftwareVersion() {
return otaSoftwareVersion; return otaSoftwareVersion;
} }

View File

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

View File

@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
public class Workout { public class Workout {
public static final byte id = 0x17; public static final byte id = 0x17;
public static class WorkoutCount { public static class WorkoutCount {
@ -63,6 +64,7 @@ public class Workout {
public short paceCount; public short paceCount;
public short segmentsCount = 0; public short segmentsCount = 0;
public short spO2Count = 0; public short spO2Count = 0;
public short sectionsCount = 0;
} }
@ -90,12 +92,15 @@ public class Workout {
workoutNumber.workoutNumber = subContainerTlv.getShort(0x06); workoutNumber.workoutNumber = subContainerTlv.getShort(0x06);
workoutNumber.dataCount = subContainerTlv.getShort(0x07); workoutNumber.dataCount = subContainerTlv.getShort(0x07);
workoutNumber.paceCount = subContainerTlv.getShort(0x08); workoutNumber.paceCount = subContainerTlv.getShort(0x08);
if(subContainerTlv.contains(0x09)) { if (subContainerTlv.contains(0x09)) {
workoutNumber.segmentsCount = subContainerTlv.getShort(0x09); workoutNumber.segmentsCount = subContainerTlv.getShort(0x09);
} }
if(subContainerTlv.contains(0x0c)) { if (subContainerTlv.contains(0x0c)) {
workoutNumber.spO2Count = subContainerTlv.getShort(0x0c); workoutNumber.spO2Count = subContainerTlv.getShort(0x0c);
} }
if (subContainerTlv.contains(0x0d)) {
workoutNumber.sectionsCount = subContainerTlv.getShort(0x0d);
}
this.workoutNumbers.add(workoutNumber); this.workoutNumbers.add(workoutNumber);
} }
@ -285,17 +290,23 @@ public class Workout {
public Request( public Request(
ParamsProvider paramsProvider, ParamsProvider paramsProvider,
short workoutNumber, short workoutNumber,
short dataNumber short dataNumber,
boolean newSteps
) { ) {
super(paramsProvider); super(paramsProvider);
this.serviceId = Workout.id; this.serviceId = Workout.id;
this.commandId = id; this.commandId = id;
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV() HuaweiTLV data = new HuaweiTLV()
.put(0x02, workoutNumber) .put(0x02, workoutNumber)
.put(0x03, dataNumber) .put(0x03, dataNumber);
); if (newSteps) {
data.put(0x06, (byte) 1);
}
data.put(0x07);
this.tlv = new HuaweiTLV().put(0x81, data);
this.complete = true; this.complete = true;
} }
@ -311,6 +322,7 @@ public class Workout {
public byte dataLength; public byte dataLength;
public short bitmap; // TODO: can this be enum-like? public short bitmap; // TODO: can this be enum-like?
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Header{" + return "Header{" +
@ -342,6 +354,17 @@ public class Workout {
public byte midFootLanding = -1; public byte midFootLanding = -1;
public byte backFootLanding = -1; public byte backFootLanding = -1;
public byte eversionAngle = -1; public byte eversionAngle = -1;
public short hangTime = -1;
public short impactHangRate = -1;
public byte rideCadence = -1;
public float ap = 0.0F;
public float vo = 0.0F;
public float gtb = 0.0F;
public float vr = 0.0F;
public byte ceiling = -1;
public byte temp = -1;
public byte spo2 = -1;
public short cns = -1;
public short swolf = -1; public short swolf = -1;
public short strokeRate = -1; public short strokeRate = -1;
@ -353,6 +376,7 @@ public class Workout {
public int timestamp = -1; // Calculated timestamp for this data point public int timestamp = -1; // Calculated timestamp for this data point
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Data{" + return "Data{" +
@ -369,6 +393,17 @@ public class Workout {
", midFootLanding=" + midFootLanding + ", midFootLanding=" + midFootLanding +
", backFootLanding=" + backFootLanding + ", backFootLanding=" + backFootLanding +
", eversionAngle=" + eversionAngle + ", eversionAngle=" + eversionAngle +
", hangTime=" + hangTime +
", impactHangRate=" + impactHangRate +
", rideCadence=" + rideCadence +
", ap=" + ap +
", vo=" + vo +
", gtb=" + gtb +
", vr=" + vr +
", ceiling=" + ceiling +
", temp=" + temp +
", spo2=" + spo2 +
", cns=" + cns +
", swolf=" + swolf + ", swolf=" + swolf +
", strokeRate=" + strokeRate + ", strokeRate=" + strokeRate +
", calories=" + calories + ", calories=" + calories +
@ -387,7 +422,8 @@ public class Workout {
public short dataNumber; public short dataNumber;
public byte[] rawHeader; public byte[] rawHeader;
public byte[] rawData; public byte[] rawData;
public short innerBitmap; public int innerBitmap = 0;
public int extraDataLength = 0;
public Header header; public Header header;
public List<Data> dataList; public List<Data> dataList;
@ -416,11 +452,19 @@ public class Workout {
this.rawHeader = container.getBytes(0x04); this.rawHeader = container.getBytes(0x04);
this.rawData = container.getBytes(0x05); // TODO: not sure if 5 can also be omitted this.rawData = container.getBytes(0x05); // TODO: not sure if 5 can also be omitted
if (container.contains(0x09)) if (container.contains(0x08))
innerBitmap = container.getShort(0x09); this.extraDataLength = container.getAsInteger(0x08);
else
innerBitmap = 0x01FF; // This seems to be the default
if (container.contains(0x09))
this.innerBitmap = container.getAsInteger(0x09);
else
this.innerBitmap = 0x01FF; // This seems to be the default
if (this.rawHeader.length != 14)
throw new LengthMismatchException("Workout data header length mismatch.");
// Calculate inner data length
int innerDataLength = 0; int innerDataLength = 0;
for (byte i = 0; i < innerBitmapLengths.length; i++) { for (byte i = 0; i < innerBitmapLengths.length; i++) {
if ((innerBitmap & (1 << i)) != 0) { if ((innerBitmap & (1 << i)) != 0) {
@ -428,8 +472,9 @@ public class Workout {
} }
} }
if (this.rawHeader.length != 14) //TODO: innerDataLength should be equal to this.extraDataLength. Should correlate to innerBitmap default value.
throw new LengthMismatchException("Workout data header length mismatch."); //TODO: I suppose innerBitmap should be 0 by default but not sure and don't have devices for testing. So I do not add this check.
//TODO: is is possible that innerBitmap = 0x01FF only true for AW70 devices. in this case extraDataLength should be properly calculated.
this.header = new Header(); this.header = new Header();
ByteBuffer buf = ByteBuffer.wrap(this.rawHeader); ByteBuffer buf = ByteBuffer.wrap(this.rawHeader);
@ -517,7 +562,40 @@ public class Workout {
data.backFootLanding = buf.get(); data.backFootLanding = buf.get();
break; break;
case 8: case 8:
data.eversionAngle = buf.get(); data.eversionAngle = buf.get(); // buf.get() - 100;
break;
case 9:
data.hangTime = buf.getShort();
break;
case 10:
data.impactHangRate = buf.getShort();
break;
case 11:
data.rideCadence = buf.get();
break;
case 12:
data.ap = (float) buf.getShort() / 10.0f;
break;
case 13:
data.vo = (float) buf.getShort() / 10.0f;
break;
case 14:
data.gtb = (float) buf.getShort() / 100.0f;
break;
case 15:
data.vr = (float) buf.getShort() / 10.0f;
break;
case 16:
data.ceiling = buf.get();
break;
case 17:
data.temp = buf.get();
break;
case 18:
data.spo2 = buf.get();
break;
case 19:
data.cns = buf.getShort();
break; break;
default: default:
data.unknownData = this.tlv.serialize(); data.unknownData = this.tlv.serialize();
@ -586,6 +664,7 @@ public class Workout {
public short correction = 0; public short correction = 0;
public boolean hasCorrection = false; public boolean hasCorrection = false;
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Block{" + return "Block{" +
@ -663,25 +742,24 @@ public class Workout {
public int pace = -1; public int pace = -1;
public short pointIndex = 0; public short pointIndex = 0;
public short segment = -1; public short segment = -1;
public byte swimType= -1; public byte swimType = -1;
public short strokes = -1; public short strokes = -1;
public short avgSwolf = -1; public short avgSwolf = -1;
public int time= -1; public int time = -1;
@NonNull
@Override @Override
public String toString() { public String toString() {
final StringBuffer sb = new StringBuffer("Block{"); return "Block{" + "distance=" + distance +
sb.append("distance=").append(distance); ", type=" + type +
sb.append(", type=").append(type); ", pace=" + pace +
sb.append(", pace=").append(pace); ", pointIndex=" + pointIndex +
sb.append(", pointIndex=").append(pointIndex); ", segment=" + segment +
sb.append(", segment=").append(segment); ", swimType=" + swimType +
sb.append(", swimType=").append(swimType); ", strokes=" + strokes +
sb.append(", strokes=").append(strokes); ", awgSwolf=" + avgSwolf +
sb.append(", awgSwolf=").append(avgSwolf); ", time=" + time +
sb.append(", time=").append(time); '}';
sb.append('}');
return sb.toString();
} }
} }
@ -712,13 +790,13 @@ public class Workout {
if (blockTlv.contains(0x09)) if (blockTlv.contains(0x09))
block.segment = blockTlv.getShort(0x09); block.segment = blockTlv.getShort(0x09);
if (blockTlv.contains(0x0a)) if (blockTlv.contains(0x0a))
block.swimType= blockTlv.getByte(0x0a); block.swimType = blockTlv.getByte(0x0a);
if (blockTlv.contains(0x0b)) if (blockTlv.contains(0x0b))
block.strokes = blockTlv.getShort(0x0b); block.strokes = blockTlv.getShort(0x0b);
if (blockTlv.contains(0x0c)) if (blockTlv.contains(0x0c))
block.avgSwolf = blockTlv.getShort(0x0c); block.avgSwolf = blockTlv.getShort(0x0c);
if (blockTlv.contains(0x0d)) if (blockTlv.contains(0x0d))
block.time= blockTlv.getInteger(0x0d); block.time = blockTlv.getInteger(0x0d);
blocks.add(block); blocks.add(block);
} }
@ -758,11 +836,9 @@ public class Workout {
@NonNull @NonNull
@Override @Override
public String toString() { public String toString() {
final StringBuffer sb = new StringBuffer("Block{"); return "Block{" + "interval=" + interval +
sb.append("interval=").append(interval); ", value=" + value +
sb.append(", value=").append(value); '}';
sb.append('}');
return sb.toString();
} }
} }
@ -797,6 +873,179 @@ public class Workout {
} }
} }
public static class WorkoutCapability {
public static final int id = 0x15;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider
) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public boolean supportNewStep = false;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
if (this.tlv.contains(0x01)) {
int flags = this.tlv.getAsInteger(0x01);
supportNewStep = (flags & 2) == 2;
}
}
}
}
public static class WorkoutSections {
public static final int id = 0x16;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
short workoutNumber,
short additionalDataNumber
) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, workoutNumber)
.put(0x02, additionalDataNumber)
.put(0x03);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public static class Block {
public int num = -1;
public long time = -1;
public long distance = -1;
public int pace = -1;
public int heartRate = -1;
public int cadence = -1;
public int stepLength = -1;
public long totalRise = -1;
public long totalDescend = -1;
public int groundContactTime = -1;
public int groundImpact = -1;
public int swingAngle = -1;
public int eversion = -1;
public int avgCadence = -1;
public int intervalTrainingType = -1;
public int divingMaxDepth = -1;
public int divingUnderwaterTime = -1;
public int divingBreakTime = -1;
@NonNull
@Override
public String toString() {
return "Block{" + "num=" + num +
", time=" + time +
", distance=" + distance +
", pace=" + pace +
", heartRate=" + heartRate +
", cadence=" + cadence +
", stepLength=" + stepLength +
", totalRise=" + totalRise +
", totalDescend=" + totalDescend +
", groundContactTime=" + groundContactTime +
", groundImpact=" + groundImpact +
", swingAngle=" + swingAngle +
", eversion=" + eversion +
", avgCadence=" + avgCadence +
", intervalTrainingType=" + intervalTrainingType +
", divingMaxDepth=" + divingMaxDepth +
", divingUnderwaterTime=" + divingUnderwaterTime +
", divingBreakTime=" + divingBreakTime +
'}';
}
}
public short workoutId;
public short number; //TODO: meaning of this field
public List<Block> blocks;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
this.workoutId = this.tlv.getShort(0x01);
this.number = 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.num = blockTlv.getAsInteger(0x05);
if (blockTlv.contains(0x06))
block.time = blockTlv.getAsLong(0x06);
if (blockTlv.contains(0x07))
block.distance = blockTlv.getAsLong(0x07);
if (blockTlv.contains(0x08))
block.pace = blockTlv.getAsInteger(0x08);
if (blockTlv.contains(0x09))
block.heartRate = blockTlv.getAsInteger(0x09);
if (blockTlv.contains(0xa))
block.cadence = blockTlv.getAsInteger(0xa);
if (blockTlv.contains(0xb))
block.stepLength = blockTlv.getAsInteger(0xb);
if (blockTlv.contains(0xc))
block.totalRise = blockTlv.getAsLong(0xc);
if (blockTlv.contains(0xd))
block.totalDescend = blockTlv.getAsLong(0xd);
if (blockTlv.contains(0xe))
block.groundContactTime = blockTlv.getAsInteger(0xe);
if (blockTlv.contains(0xf))
block.groundImpact = blockTlv.getAsInteger(0xf);
if (blockTlv.contains(0x10))
block.swingAngle = blockTlv.getAsInteger(0x10);
if (blockTlv.contains(0x11))
block.eversion = blockTlv.getAsInteger(0x11);
if (blockTlv.contains(0x22))
block.avgCadence = blockTlv.getAsInteger(0x22);
if (blockTlv.contains(0x23))
block.intervalTrainingType = blockTlv.getAsInteger(0x23);
if (blockTlv.contains(0x28))
block.divingMaxDepth = blockTlv.getAsInteger(0x28);
if (blockTlv.contains(0x29))
block.divingUnderwaterTime = blockTlv.getAsInteger(0x29);
if (blockTlv.contains(0x2a))
block.divingBreakTime = blockTlv.getAsInteger(0x2a);
blocks.add(block);
}
}
}
}
public static class NotifyHeartRate { public static class NotifyHeartRate {
public static final int id = 0x17; 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.HuaweiWorkoutDataSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutPaceSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSectionsSample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSectionsSampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2Sample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2Sample;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao;
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
@ -128,6 +130,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetN
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetOTAChangeLog; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetOTAChangeLog;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetSmartAlarmList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfaceParams; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWatchfaceParams;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetWorkoutCapability;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendCameraRemoteSetupEvent; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendCameraRemoteSetupEvent;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendDeviceReportThreshold; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendDeviceReportThreshold;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest;
@ -856,6 +859,7 @@ public class HuaweiSupportProvider {
initRequestQueue.add(new GetContactsCount(this)); initRequestQueue.add(new GetContactsCount(this));
initRequestQueue.add(new SendOTASetAutoUpdate(this)); initRequestQueue.add(new SendOTASetAutoUpdate(this));
initRequestQueue.add(new GetOTAChangeLog(this)); initRequestQueue.add(new GetOTAChangeLog(this));
//initRequestQueue.add(new GetWorkoutCapability(this)); // TODO: in current stage I don't understand how to parse new steps.
initRequestQueue.add(new GetEventAlarmList(this)); initRequestQueue.add(new GetEventAlarmList(this));
initRequestQueue.add(new GetSmartAlarmList(this)); initRequestQueue.add(new GetSmartAlarmList(this));
@ -1821,7 +1825,18 @@ public class HuaweiSupportProvider {
data.calories, data.calories,
data.cyclingPower, data.cyclingPower,
data.frequency, data.frequency,
data.altitude data.altitude,
data.hangTime,
data.impactHangRate,
data.rideCadence,
data.ap,
data.vo,
data.gtb,
data.vr,
data.ceiling,
data.temp,
data.spo2,
data.cns
); );
dao.insertOrReplace(dataSample); dao.insertOrReplace(dataSample);
} }
@ -1928,6 +1943,54 @@ public class HuaweiSupportProvider {
} }
} }
public void addWorkoutSectionsData(Long workoutId, List<Workout.WorkoutSections.Response.Block> spO2List, short number) {
if (workoutId == null)
return;
// NOTE: All fields of this data is optional. At this point I don't all workouts that this data used.
// I decided to add two additional fields dataIdx and rowIdx as primary keys that should identify each row
try (DBHandler db = GBApplication.acquireDB()) {
HuaweiWorkoutSectionsSampleDao dao = db.getDaoSession().getHuaweiWorkoutSectionsSampleDao();
if (number == 0) {
final DeleteQuery<HuaweiWorkoutSectionsSample> tableDeleteQuery = dao.queryBuilder()
.where(HuaweiWorkoutSectionsSampleDao.Properties.WorkoutId.eq(workoutId))
.buildDelete();
tableDeleteQuery.executeDeleteWithoutDetachingEntities();
}
int i = 0;
for (Workout.WorkoutSections.Response.Block block : spO2List) {
HuaweiWorkoutSectionsSample huaweiWorkoutSectionsSample = new HuaweiWorkoutSectionsSample(
workoutId,
number,
i++,
block.num,
block.time,
block.distance,
block.pace,
block.heartRate,
block.cadence,
block.stepLength,
block.totalRise,
block.totalDescend,
block.groundContactTime,
block.groundImpact,
block.swingAngle,
block.eversion,
block.avgCadence,
block.divingUnderwaterTime,
block.divingMaxDepth,
block.divingUnderwaterTime,
block.divingBreakTime
);
dao.insertOrReplace(huaweiWorkoutSectionsSample);
}
} catch (Exception e) {
LOG.error("Failed to add workout sections data to database", e);
}
}
public void addDictData(List<HuaweiP2PDataDictionarySyncService.DictData> dictData) { public void addDictData(List<HuaweiP2PDataDictionarySyncService.DictData> dictData) {
try (DBHandler db = GBApplication.acquireDB()) { try (DBHandler db = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(db.getDaoSession()).getId(); Long userId = DBHelper.getUser(db.getDaoSession()).getId();

View File

@ -301,7 +301,18 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
responseData.calories, responseData.calories,
responseData.cyclingPower, responseData.cyclingPower,
responseData.frequency, responseData.frequency,
responseData.altitude responseData.altitude,
responseData.hangTime,
responseData.impactHangRate,
responseData.rideCadence,
responseData.ap,
responseData.vo,
responseData.gtb,
responseData.vr,
responseData.ceiling,
responseData.temp,
responseData.spo2,
responseData.cns
); );
dbHandler.getDaoSession().getHuaweiWorkoutDataSampleDao().insertOrReplace(dataSample); dbHandler.getDaoSession().getHuaweiWorkoutDataSampleDao().insertOrReplace(dataSample);

View File

@ -0,0 +1,45 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
public class GetWorkoutCapability extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutCapability.class);
public GetWorkoutCapability(HuaweiSupportProvider support) {
super(support);
this.serviceId = Workout.id;
this.commandId = Workout.WorkoutCapability.id;
}
@Override
protected boolean requestSupported() {
return supportProvider.getHuaweiCoordinator().supportsWorkoutCapability();
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutCapability.Request(paramsProvider).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
@Override
protected void processResponse() throws ResponseParseException {
LOG.debug("handle WorkoutCapability");
if (!(receivedPacket instanceof Workout.WorkoutCapability.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutCapability.Response.class);
LOG.info("Workout capability: NewSteps: {}", ((Workout.WorkoutCapability.Response) receivedPacket).supportNewStep);
supportProvider.getHuaweiCoordinator().setSupportsWorkoutNewSteps(((Workout.WorkoutCapability.Response) receivedPacket).supportNewStep);
}
}

View File

@ -60,7 +60,7 @@ public class GetWorkoutDataRequest extends Request {
@Override @Override
protected List<byte[]> createRequest() throws RequestCreationException { protected List<byte[]> createRequest() throws RequestCreationException {
try { try {
return new Workout.WorkoutData.Request(paramsProvider, workoutNumbers.workoutNumber, this.number).serialize(); return new Workout.WorkoutData.Request(paramsProvider, workoutNumbers.workoutNumber, this.number, supportProvider.getHuaweiCoordinator().isSupportsWorkoutNewSteps()).serialize();
} catch (HuaweiPacket.CryptoException e) { } catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e); throw new RequestCreationException(e);
} }
@ -133,6 +133,16 @@ public class GetWorkoutDataRequest extends Request {
); );
nextRequest.setFinalizeReq(this.finalizeReq); nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest); this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else { } else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {

View File

@ -109,6 +109,16 @@ public class GetWorkoutPaceRequest extends Request {
); );
nextRequest.setFinalizeReq(this.finalizeReq); nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest); this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else { } else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {

View File

@ -0,0 +1,96 @@
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 GetWorkoutSectionsRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(GetWorkoutSectionsRequest.class);
Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers;
List<Workout.WorkoutCount.Response.WorkoutNumbers> remainder;
short number;
Long databaseId;
public GetWorkoutSectionsRequest(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.WorkoutSections.id;
this.workoutNumbers = workoutNumbers;
this.remainder = remainder;
this.number = number;
this.databaseId = databaseId;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new Workout.WorkoutSections.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.WorkoutSections.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutSections.Response.class);
Workout.WorkoutSections.Response packet = (Workout.WorkoutSections.Response) receivedPacket;
LOG.info("Workout {} section {}:", this.workoutNumbers.workoutNumber, this.number);
LOG.info("workoutId : {}", packet.workoutId);
LOG.info("number : {}", packet.number);
LOG.info("Block num : {}", packet.blocks.size());
LOG.info("Blocks : {}", Arrays.toString(packet.blocks.toArray()));
supportProvider.addWorkoutSectionsData(this.databaseId, packet.blocks, this.number);
if (this.workoutNumbers.sectionsCount > this.number + 1) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
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(
GetWorkoutSectionsRequest.this.supportProvider,
remainder.remove(0),
remainder
);
nextRequest.setFinalizeReq(GetWorkoutSectionsRequest.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

@ -45,7 +45,7 @@ public class GetWorkoutSpO2Request extends Request {
@Override @Override
protected void processResponse() throws ResponseParseException { protected void processResponse() throws ResponseParseException {
if (!(receivedPacket instanceof Workout.WorkoutSpO2.Response)) if (!(receivedPacket instanceof Workout.WorkoutSpO2.Response))
throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutSwimSegments.Response.class); throw new ResponseTypeMismatchException(receivedPacket, Workout.WorkoutSections.Response.class);
Workout.WorkoutSpO2.Response packet = (Workout.WorkoutSpO2.Response) receivedPacket; Workout.WorkoutSpO2.Response packet = (Workout.WorkoutSpO2.Response) receivedPacket;
@ -68,6 +68,16 @@ public class GetWorkoutSpO2Request extends Request {
); );
nextRequest.setFinalizeReq(this.finalizeReq); nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest); this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else { } else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {

View File

@ -83,6 +83,16 @@ public class GetWorkoutSwimSegmentsRequest extends Request {
); );
nextRequest.setFinalizeReq(this.finalizeReq); nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest); this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
this.databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else { } else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() {

View File

@ -124,6 +124,16 @@ public class GetWorkoutTotalsRequest extends Request {
); );
nextRequest.setFinalizeReq(this.finalizeReq); nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest); this.nextRequest(nextRequest);
} else if (this.workoutNumbers.sectionsCount > 0) {
GetWorkoutSectionsRequest nextRequest = new GetWorkoutSectionsRequest(
this.supportProvider,
this.workoutNumbers,
this.remainder,
(short) 0,
databaseId
);
nextRequest.setFinalizeReq(this.finalizeReq);
this.nextRequest(nextRequest);
} else { } else {
new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(databaseId); new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(databaseId);
supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, databaseId, new Runnable() { supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, databaseId, new Runnable() {