From 1f1c598968af49a9f1f16e71e94798baab0f6a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Fri, 13 Feb 2026 22:36:43 +0000 Subject: [PATCH] Huawei: Improve workout parsing (#5784) Original patch by Me7c7 in #5784 Co-authored-by: Me7c7 --- .../devices/huawei/HuaweiPacket.java | 12 +- .../devices/huawei/packets/Workout.java | 200 ++++++++++-------- .../devices/huawei/HuaweiSupportProvider.java | 8 +- 3 files changed, 121 insertions(+), 99 deletions(-) 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 96896eb5b0..5bea29faad 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 @@ -76,7 +76,9 @@ public class HuaweiPacket { protected byte authAlgo; protected byte encryptMethod; protected byte[] firstKey; - protected byte authMode; + protected byte authMode; + + protected boolean isAW = false; public void setAuthVersion(byte authVersion) { this.authVersion = authVersion; @@ -184,6 +186,14 @@ public class HuaweiPacket { public byte[] getFirstKey() { return firstKey; } + + public boolean isAW() { + return isAW; + } + + public void setAW(boolean AW) { + isAW = AW; + } } public static abstract class ParseException extends Exception { 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 97d3389914..1a2f32091f 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 @@ -324,11 +324,11 @@ public class Workout { this.recoveryHeartRates = container.getBytes(0x66); for (HuaweiTLV subTlv : container.getObjects(0xe7)) { - if(subTlv.contains(0x68) && subTlv.contains(0x69)) { + if (subTlv.contains(0x68) && subTlv.contains(0x69)) { int tag = subTlv.getInteger(0x68); String value = subTlv.getString(0x69); // The watch returns this value always as string. String key = huaweiIdToKey.get(tag); - if(key != null) + if (key != null) additionalValues.put(key, value); } } @@ -470,7 +470,7 @@ public class Workout { } private final byte[] bitmapLengths = {1, 2, 1, 2, 2, 4, -1, 2, 2, 2}; - private final byte[] innerBitmapLengths = {2, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2, 1, 2, 2, 2, 2, 1, 1, 1, 2}; + private final byte[] innerBitmapLengths = {2, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2, 1, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 2}; public Integer error = null; @@ -479,7 +479,7 @@ public class Workout { public byte[] rawHeader; public byte[] rawData; public int innerBitmap = 0; - public int extraDataLength = 0; + public int innerDataLength = 0; public Header header; public List dataList; @@ -499,6 +499,92 @@ public class Workout { this.parseTlv(); } + private void parseInnerData(byte[] innerRawData, Data data) { + ByteBuffer buf = ByteBuffer.wrap(innerRawData); + // Inner data, parsing into data + + for (byte k = 0; k < innerBitmapLengths.length; k++) { + if ((innerBitmap & (1 << k)) != 0) { + switch (k) { + case 0: + data.cadence = buf.getShort(); + break; + case 1: + data.stepLength = buf.getShort(); + break; + case 2: + data.groundContactTime = buf.getShort(); + break; + case 3: + data.impact = buf.get(); + break; + case 4: + data.swingAngle = buf.getShort(); + break; + case 5: + data.foreFootLanding = buf.get(); + break; + case 6: + data.midFootLanding = buf.get(); + break; + case 7: + data.backFootLanding = buf.get(); + break; + case 8: + data.eversionAngle = buf.get(); + 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; +// case 20: +// buf.getShort(); //cycle analog power +// break; +// case 21: +// buf.get(); // Unknown +// break; +// case 22: +// buf.getShort(); // cycle analog cadence +// break; + default: + break; + } + } + } + if (buf.remaining() > 0) { + data.unknownData = this.tlv.serialize(); + } + } + @Override public void parseTlv() throws ParseException { @@ -515,29 +601,18 @@ public class Workout { this.rawData = container.getBytes(0x05); // TODO: not sure if 5 can also be omitted if (container.contains(0x08)) - this.extraDataLength = container.getAsInteger(0x08); + this.innerDataLength = container.getAsInteger(0x08); + else if(paramsProvider.isAW()) //NOTE: bitmap 0x01FF and length 26 only true for AW devices + this.innerDataLength = 26; if (container.contains(0x09)) this.innerBitmap = container.getAsInteger(0x09); - else + else if(paramsProvider.isAW()) //NOTE: bitmap 0x01FF and length 26 only true for AW devices 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) { - innerDataLength += innerBitmapLengths[i]; - } - } - - //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); header.workoutNumber = buf.getShort(); @@ -552,6 +627,17 @@ public class Workout { if (this.header.dataCount * this.header.dataLength != this.rawData.length) throw new LengthMismatchException("Workout data length mismatch with header."); + // Calculate inner data length + int calcInnerDataLength = 0; + for (byte i = 0; i < innerBitmapLengths.length; i++) { + if ((innerBitmap & (1 << i)) != 0) { + calcInnerDataLength += innerBitmapLengths[i]; + } + } + + if(calcInnerDataLength > this.innerDataLength) + throw new LengthMismatchException("Workout data innerDataLength incorrect"); + // Check data lengths from bitmap int dataLength = 0; for (byte i = 0; i < bitmapLengths.length; i++) { @@ -595,78 +681,10 @@ public class Workout { break; case 6: // Inner data, parsing into data - // TODO: function for readability? - for (byte k = 0; k < innerBitmapLengths.length; k++) { - if ((innerBitmap & (1 << k)) != 0) { - switch (k) { - case 0: - data.cadence = buf.getShort(); - break; - case 1: - data.stepLength = buf.getShort(); - break; - case 2: - data.groundContactTime = buf.getShort(); - break; - case 3: - data.impact = buf.get(); - break; - case 4: - data.swingAngle = buf.getShort(); - break; - case 5: - data.foreFootLanding = buf.get(); - break; - case 6: - data.midFootLanding = buf.get(); - break; - case 7: - data.backFootLanding = buf.get(); - break; - case 8: - 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(); - // Fix alignment - for (int l = 0; l < innerBitmapLengths[k]; l++) - buf.get(); - break; - } - } + if (innerDataLength > 0) { + byte[] innerRawData = new byte[innerDataLength]; + buf.get(innerRawData); + parseInnerData(innerRawData, data); } break; case 7: 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 53b610a7ee..b238f94da5 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 @@ -393,6 +393,7 @@ public class HuaweiSupportProvider { this.gbDevice = device; this.context = context; this.huaweiType = getCoordinator().getHuaweiType(); + this.paramsProvider.setAW(getCoordinator().getHuaweiType() == HuaweiDeviceType.AW); this.paramsProvider.setTransactionsCrypted(getCoordinator().isTransactionCrypted()); mediaManager = new MediaManager(context); } @@ -473,13 +474,6 @@ public class HuaweiSupportProvider { this.gpsParametersResponse = response; } - public void setup(GBDevice device, Context context) { - this.gbDevice = device; - this.context = context; - this.huaweiType = getCoordinator().getHuaweiType(); - this.paramsProvider.setTransactionsCrypted(getCoordinator().isTransactionCrypted()); - } - protected nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder initializeDevice(nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder builder) { builder.setCallback(leSupport); final BluetoothGattCharacteristic characteristicRead = leSupport.getCharacteristic(HuaweiConstants.UUID_CHARACTERISTIC_HUAWEI_READ);