diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 24ecfd327..91a638f5c 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(95, MAIN_PACKAGE + ".entities"); + final Schema schema = new Schema(96, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -161,6 +161,7 @@ public class GBDaoGenerator { addHuaweiWorkoutPaceSample(schema, huaweiWorkoutSummary); addHuaweiWorkoutSwimSegmentsSample(schema, huaweiWorkoutSummary); addHuaweiWorkoutSpO2Sample(schema, huaweiWorkoutSummary); + addHuaweiWorkoutSectionsSample(schema, huaweiWorkoutSummary); Entity huaweiDictData = addHuaweiDictData(schema, user, device); addHuaweiDictDataValues(schema, huaweiDictData); @@ -1502,6 +1503,18 @@ public class GBDaoGenerator { workoutDataSample.addShortProperty("frequency").notNull(); 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; } @@ -1559,6 +1572,39 @@ public class GBDaoGenerator { 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) { Entity dictData = addEntity(schema, "HuaweiDictData"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_96.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_96.java new file mode 100644 index 000000000..56d73f48d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/database/schema/GadgetbridgeUpdate_96.java @@ -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) { + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java index d7834456e..822832e4a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiCoordinator.java @@ -75,6 +75,8 @@ public class HuaweiCoordinator { private boolean supportsTruSleepNewSync = false; private boolean supportsGpsNewSync = false; + private boolean supportsWorkoutNewSteps = false; + private Watchface.WatchfaceDeviceParams watchfaceDeviceParams; private App.AppDeviceParams appDeviceParams; @@ -585,6 +587,10 @@ public class HuaweiCoordinator { return supportsCommandForService(0x17, 0x01); } + public boolean supportsWorkoutCapability() { + return supportsCommandForService(0x17, 0x15); + } + public boolean supportsWorkoutsTrustHeartRate() { return supportsCommandForService(0x17, 0x17); } @@ -966,6 +972,14 @@ public class HuaweiCoordinator { this.supportsGpsNewSync = supportsGpsNewSync; } + public boolean isSupportsWorkoutNewSteps() { + return supportsWorkoutNewSteps; + } + + public void setSupportsWorkoutNewSteps(boolean supportsWorkoutNewSteps) { + this.supportsWorkoutNewSteps = supportsWorkoutNewSteps; + } + public String getOtaSoftwareVersion() { return otaSoftwareVersion; } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java index b3d3b52a6..263a9f571 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiPacket.java @@ -582,6 +582,10 @@ public class HuaweiPacket { return new Workout.WorkoutSwimSegments.Response(paramsProvider).fromPacket(this); case Workout.WorkoutSpO2.id: 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: this.isEncrypted = this.attemptDecrypt(); // Helps with debugging return this; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java index 0d364327c..820ce0555 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/packets/Workout.java @@ -28,6 +28,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; public class Workout { + public static final byte id = 0x17; public static class WorkoutCount { @@ -63,6 +64,7 @@ public class Workout { public short paceCount; public short segmentsCount = 0; public short spO2Count = 0; + public short sectionsCount = 0; } @@ -90,12 +92,15 @@ public class Workout { workoutNumber.workoutNumber = subContainerTlv.getShort(0x06); workoutNumber.dataCount = subContainerTlv.getShort(0x07); workoutNumber.paceCount = subContainerTlv.getShort(0x08); - if(subContainerTlv.contains(0x09)) { + if (subContainerTlv.contains(0x09)) { workoutNumber.segmentsCount = subContainerTlv.getShort(0x09); } - if(subContainerTlv.contains(0x0c)) { + if (subContainerTlv.contains(0x0c)) { workoutNumber.spO2Count = subContainerTlv.getShort(0x0c); } + if (subContainerTlv.contains(0x0d)) { + workoutNumber.sectionsCount = subContainerTlv.getShort(0x0d); + } this.workoutNumbers.add(workoutNumber); } @@ -285,17 +290,23 @@ public class Workout { public Request( ParamsProvider paramsProvider, short workoutNumber, - short dataNumber + short dataNumber, + boolean newSteps ) { super(paramsProvider); this.serviceId = Workout.id; this.commandId = id; - this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV() + HuaweiTLV data = new HuaweiTLV() .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; } @@ -311,6 +322,7 @@ public class Workout { public byte dataLength; public short bitmap; // TODO: can this be enum-like? + @NonNull @Override public String toString() { return "Header{" + @@ -342,6 +354,17 @@ public class Workout { public byte midFootLanding = -1; public byte backFootLanding = -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 strokeRate = -1; @@ -353,6 +376,7 @@ public class Workout { public int timestamp = -1; // Calculated timestamp for this data point + @NonNull @Override public String toString() { return "Data{" + @@ -369,6 +393,17 @@ public class Workout { ", midFootLanding=" + midFootLanding + ", backFootLanding=" + backFootLanding + ", 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 + ", strokeRate=" + strokeRate + ", calories=" + calories + @@ -387,7 +422,8 @@ public class Workout { public short dataNumber; public byte[] rawHeader; public byte[] rawData; - public short innerBitmap; + public int innerBitmap = 0; + public int extraDataLength = 0; public Header header; public List dataList; @@ -416,11 +452,19 @@ public class Workout { this.rawHeader = container.getBytes(0x04); this.rawData = container.getBytes(0x05); // TODO: not sure if 5 can also be omitted - if (container.contains(0x09)) - innerBitmap = container.getShort(0x09); - else - innerBitmap = 0x01FF; // This seems to be the default + if (container.contains(0x08)) + this.extraDataLength = container.getAsInteger(0x08); + 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; for (byte i = 0; i < innerBitmapLengths.length; i++) { if ((innerBitmap & (1 << i)) != 0) { @@ -428,8 +472,9 @@ public class Workout { } } - if (this.rawHeader.length != 14) - throw new LengthMismatchException("Workout data header length mismatch."); + //TODO: innerDataLength should be equal to this.extraDataLength. Should correlate to innerBitmap default value. + //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(); ByteBuffer buf = ByteBuffer.wrap(this.rawHeader); @@ -517,7 +562,40 @@ public class Workout { data.backFootLanding = buf.get(); break; 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; default: data.unknownData = this.tlv.serialize(); @@ -586,6 +664,7 @@ public class Workout { public short correction = 0; public boolean hasCorrection = false; + @NonNull @Override public String toString() { return "Block{" + @@ -663,25 +742,24 @@ public class Workout { public int pace = -1; public short pointIndex = 0; public short segment = -1; - public byte swimType= -1; + public byte swimType = -1; public short strokes = -1; public short avgSwolf = -1; - public int time= -1; + public int time = -1; + @NonNull @Override public String toString() { - final StringBuffer sb = new StringBuffer("Block{"); - sb.append("distance=").append(distance); - sb.append(", type=").append(type); - sb.append(", pace=").append(pace); - sb.append(", pointIndex=").append(pointIndex); - sb.append(", segment=").append(segment); - sb.append(", swimType=").append(swimType); - sb.append(", strokes=").append(strokes); - sb.append(", awgSwolf=").append(avgSwolf); - sb.append(", time=").append(time); - sb.append('}'); - return sb.toString(); + return "Block{" + "distance=" + distance + + ", type=" + type + + ", pace=" + pace + + ", pointIndex=" + pointIndex + + ", segment=" + segment + + ", swimType=" + swimType + + ", strokes=" + strokes + + ", awgSwolf=" + avgSwolf + + ", time=" + time + + '}'; } } @@ -712,13 +790,13 @@ public class Workout { if (blockTlv.contains(0x09)) block.segment = blockTlv.getShort(0x09); if (blockTlv.contains(0x0a)) - block.swimType= blockTlv.getByte(0x0a); + block.swimType = blockTlv.getByte(0x0a); if (blockTlv.contains(0x0b)) block.strokes = blockTlv.getShort(0x0b); if (blockTlv.contains(0x0c)) block.avgSwolf = blockTlv.getShort(0x0c); if (blockTlv.contains(0x0d)) - block.time= blockTlv.getInteger(0x0d); + block.time = blockTlv.getInteger(0x0d); blocks.add(block); } @@ -758,11 +836,9 @@ public class Workout { @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(); + return "Block{" + "interval=" + interval + + ", value=" + value + + '}'; } } @@ -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 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 final int id = 0x17; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java index c8ccf7be5..ea4c2a306 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiSupportProvider.java @@ -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.HuaweiWorkoutSectionsSample; +import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSectionsSampleDao; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2Sample; import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSpO2SampleDao; 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.GetSmartAlarmList; 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.SendDeviceReportThreshold; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendExtendedAccountRequest; @@ -856,6 +859,7 @@ public class HuaweiSupportProvider { initRequestQueue.add(new GetContactsCount(this)); initRequestQueue.add(new SendOTASetAutoUpdate(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 GetSmartAlarmList(this)); @@ -1821,7 +1825,18 @@ public class HuaweiSupportProvider { data.calories, data.cyclingPower, 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); } @@ -1928,6 +1943,54 @@ public class HuaweiSupportProvider { } } + public void addWorkoutSectionsData(Long workoutId, List 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 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 dictData) { try (DBHandler db = GBApplication.acquireDB()) { Long userId = DBHelper.getUser(db.getDaoSession()).getId(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java index ffe48cee2..c05581f3e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiWorkoutGbParser.java @@ -301,7 +301,18 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser { responseData.calories, responseData.cyclingPower, 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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutCapability.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutCapability.java new file mode 100644 index 000000000..1e10e0558 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutCapability.java @@ -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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java index 338a921ee..afb3ad4cd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutDataRequest.java @@ -60,7 +60,7 @@ public class GetWorkoutDataRequest extends Request { @Override protected List createRequest() throws RequestCreationException { 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) { throw new RequestCreationException(e); } @@ -133,6 +133,16 @@ public class GetWorkoutDataRequest extends Request { ); nextRequest.setFinalizeReq(this.finalizeReq); 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 { new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java index 96842fe40..090934d57 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutPaceRequest.java @@ -109,6 +109,16 @@ public class GetWorkoutPaceRequest extends Request { ); nextRequest.setFinalizeReq(this.finalizeReq); 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 { new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSectionsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSectionsRequest.java new file mode 100644 index 000000000..d4d43b96d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSectionsRequest.java @@ -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 remainder; + short number; + Long databaseId; + + public GetWorkoutSectionsRequest(HuaweiSupportProvider support, Workout.WorkoutCount.Response.WorkoutNumbers workoutNumbers, List 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 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(); + } + } + }); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSpO2Request.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSpO2Request.java index cc936bd95..b5df56844 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSpO2Request.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSpO2Request.java @@ -45,7 +45,7 @@ public class GetWorkoutSpO2Request extends Request { @Override protected void processResponse() throws ResponseParseException { 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; @@ -68,6 +68,16 @@ public class GetWorkoutSpO2Request extends Request { ); nextRequest.setFinalizeReq(this.finalizeReq); 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 { new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSwimSegmentsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSwimSegmentsRequest.java index 2a6d81ca1..2949300ff 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSwimSegmentsRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutSwimSegmentsRequest.java @@ -83,6 +83,16 @@ public class GetWorkoutSwimSegmentsRequest extends Request { ); nextRequest.setFinalizeReq(this.finalizeReq); 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 { new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(this.databaseId); supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, this.databaseId, new Runnable() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java index 98ee344c1..f514658b6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/requests/GetWorkoutTotalsRequest.java @@ -124,6 +124,16 @@ public class GetWorkoutTotalsRequest extends Request { ); nextRequest.setFinalizeReq(this.finalizeReq); 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 { new HuaweiWorkoutGbParser(getDevice(), getContext()).parseWorkout(databaseId); supportProvider.downloadWorkoutGpsFiles(this.workoutNumbers.workoutNumber, databaseId, new Runnable() {