/* Copyright (C) 2024 Damien Gaignon, Martin.JM
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
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 {
public static final byte id = 0x07;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
int start,
int end
) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x81, new HuaweiTLV()
.put(0x03, start)
.put(0x04, end)
);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public static class WorkoutNumbers {
public byte[] rawData;
public short workoutNumber;
public short dataCount;
public short paceCount;
public short segmentsCount = 0;
}
public short count;
public List workoutNumbers;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
this.count = container.getShort(0x02);
this.workoutNumbers = new ArrayList<>();
if (this.count == 0)
return;
List subContainers = container.getObjects(0x85);
for (HuaweiTLV subContainerTlv : subContainers) {
WorkoutNumbers workoutNumber = new WorkoutNumbers();
workoutNumber.rawData = subContainerTlv.serialize();
workoutNumber.workoutNumber = subContainerTlv.getShort(0x06);
workoutNumber.dataCount = subContainerTlv.getShort(0x07);
workoutNumber.paceCount = subContainerTlv.getShort(0x08);
if(subContainerTlv.contains(0x09)) {
workoutNumber.segmentsCount = subContainerTlv.getShort(0x09);
}
this.workoutNumbers.add(workoutNumber);
}
// Has to be sorted for the timestamp-based sync start that we use in the HuaweiSupportProvider
this.workoutNumbers.sort(Comparator.comparingInt(o -> o.workoutNumber));
}
}
}
public static class WorkoutTotals {
public static final byte id = 0x08;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, short number) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV()
.put(0x02, number)
);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public byte[] rawData;
public short number;
public byte status = -1; // TODO: enum?
public int startTime;
public int endTime;
public int calories = -1;
public int distance = -1;
public int stepCount = -1;
public int totalTime = -1;
public int duration = -1;
public byte type = -1; // TODO: enum?
public short strokes = -1;
public short avgStrokeRate = -1;
public short poolLength = -1; // In cm
public short laps = -1;
public short avgSwolf = -1;
public Integer maxAltitude = null;
public Integer minAltitude = null;
public Integer elevationGain = null;
public Integer elevationLoss = null;
public int workoutLoad = 0;
public int workoutAerobicEffect = 0;
public byte workoutAnaerobicEffect = -1;
public short recoveryTime = 0;
public byte minHeartRatePeak = 0;
public byte maxHeartRatePeak = 0;
public byte[] recoveryHeartRates = null;
public byte swimType = -1;
public int maxMET = 0;
public byte hrZoneType = -1;
public short runPaceZone1Min = -1;
public short runPaceZone2Min = -1;
public short runPaceZone3Min = -1;
public short runPaceZone4Min = -1;
public short runPaceZone5Min = -1;
public short runPaceZone5Max = -1;
public short runPaceZone1Time = -1;
public short runPaceZone2Time = -1;
public short runPaceZone3Time = -1;
public short runPaceZone4Time = -1;
public short runPaceZone5Time = -1;
public byte algType = 0;
public int trainingPoints = -1;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
this.rawData = container.serialize();
this.number = container.getShort(0x02);
if (container.contains(0x03))
this.status = container.getByte(0x03);
this.startTime = container.getInteger(0x04);
this.endTime = container.getInteger(0x05);
if (container.contains(0x06))
this.calories = container.getInteger(0x06);
if (container.contains(0x07))
this.distance = container.getInteger(0x07);
if (container.contains(0x08))
this.stepCount = container.getInteger(0x08);
if (container.contains(0x09))
this.totalTime = container.getInteger(0x09);
if (container.contains(0x0b))
this.elevationGain = container.getInteger(0x0b);
if (container.contains(0x0c)) {
byte[] hrData = container.getBytes(0x0c);
minHeartRatePeak = hrData[0];
maxHeartRatePeak = hrData[1];
}
if (container.contains(0x0d))
this.workoutLoad = container.getInteger(0x0d);
if (container.contains(0x0e))
this.workoutAerobicEffect = container.getInteger(0x0e);
if (container.contains(0x10))
this.maxMET = container.getInteger(0x10);
if (container.contains(0x11))
this.recoveryTime = container.getShort(0x11);
if (container.contains(0x12))
this.duration = container.getInteger(0x12);
if (container.contains(0x14))
this.type = container.getByte(0x14);
if (container.contains(0x15))
this.swimType = container.getByte(0x15);
if (container.contains(0x16))
this.strokes = container.getShort(0x16);
if (container.contains(0x17))
this.avgStrokeRate = container.getShort(0x17);
if (container.contains(0x18))
this.poolLength = container.getShort(0x18);
if (container.contains(0x19))
this.laps = container.getShort(0x19);
if (container.contains(0x1a))
this.avgSwolf = container.getShort(0x1a);
if (container.contains(0x1b))
this.elevationLoss = container.getInteger(0x1b);
if (container.contains(0x1c))
this.maxAltitude = container.getInteger(0x1c);
if (container.contains(0x1d))
this.minAltitude = container.getInteger(0x1d);
if (container.contains(0x20))
this.workoutAnaerobicEffect = container.getByte(0x20);
if (container.contains(0x24))
this.hrZoneType = container.getByte(0x24);
if (container.contains(0x50))
this.runPaceZone1Min = container.getShort(0x50);
if (container.contains(0x51))
this.runPaceZone2Min = container.getShort(0x51);
if (container.contains(0x52))
this.runPaceZone3Min = container.getShort(0x52);
if (container.contains(0x53))
this.runPaceZone4Min = container.getShort(0x53);
if (container.contains(0x54))
this.runPaceZone5Min = container.getShort(0x54);
if (container.contains(0x55))
this.runPaceZone5Max = container.getShort(0x55);
if (container.contains(0x56))
this.runPaceZone1Time = container.getShort(0x56);
if (container.contains(0x57))
this.runPaceZone2Time = container.getShort(0x57);
if (container.contains(0x58))
this.runPaceZone3Time = container.getShort(0x58);
if (container.contains(0x59))
this.runPaceZone4Time = container.getShort(0x59);
if (container.contains(0x5a))
this.runPaceZone5Time = container.getShort(0x5a);
if (container.contains(0x5d))
this.algType = container.getByte(0x5d);
if (container.contains(0x63))
this.trainingPoints = container.getShort(0x63);
if (container.contains(0x66))
this.recoveryHeartRates = container.getBytes(0x66);
}
}
}
public static class WorkoutData {
public static final int id = 0x0a;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
short workoutNumber,
short dataNumber
) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV()
.put(0x02, workoutNumber)
.put(0x03, dataNumber)
);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public static class Header {
public short workoutNumber;
public short dataNumber;
public int timestamp;
public byte interval;
public short dataCount;
public byte dataLength;
public short bitmap; // TODO: can this be enum-like?
@Override
public String toString() {
return "Header{" +
"workoutNumber=" + workoutNumber +
", dataNumber=" + dataNumber +
", timestamp=" + timestamp +
", interval=" + interval +
", dataCount=" + dataCount +
", dataLength=" + dataLength +
", bitmap=" + bitmap +
'}';
}
}
public static class Data {
// If unknown data is encountered, the whole tlv will be in here so it can be parsed again later
public byte[] unknownData = null;
public byte heartRate = -1;
public short speed = -1;
public byte stepRate = -1;
public short cadence = -1;
public short stepLength = -1;
public short groundContactTime = -1;
public byte impact = -1;
public short swingAngle = -1;
public byte foreFootLanding = -1;
public byte midFootLanding = -1;
public byte backFootLanding = -1;
public byte eversionAngle = -1;
public short swolf = -1;
public short strokeRate = -1;
public short calories = -1;
public short cyclingPower = -1;
public short frequency = -1;
public Integer altitude = null;
public int timestamp = -1; // Calculated timestamp for this data point
@Override
public String toString() {
return "Data{" +
"unknownData=" + Arrays.toString(unknownData) +
", heartRate=" + heartRate +
", speed=" + speed +
", stepRate=" + stepRate +
", cadence=" + cadence +
", stepLength=" + stepLength +
", groundContactTime=" + groundContactTime +
", impact=" + impact +
", swingAngle=" + swingAngle +
", foreFootLanding=" + foreFootLanding +
", midFootLanding=" + midFootLanding +
", backFootLanding=" + backFootLanding +
", eversionAngle=" + eversionAngle +
", swolf=" + swolf +
", strokeRate=" + strokeRate +
", calories=" + calories +
", cyclingPower=" + cyclingPower +
", frequency=" + frequency +
", altitude=" + altitude +
", timestamp=" + timestamp +
'}';
}
}
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};
public short workoutNumber;
public short dataNumber;
public byte[] rawHeader;
public byte[] rawData;
public short innerBitmap;
public Header header;
public List dataList;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
/**
* This is to be able to easily reparse the error data, only accepts tlv bytes
*
* @param rawData The TLV bytes
*/
public Response(byte[] rawData) throws ParseException {
super(null);
this.tlv = new HuaweiTLV().parse(rawData);
this.parseTlv();
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
this.workoutNumber = container.getShort(0x02);
this.dataNumber = container.getShort(0x03);
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
int innerDataLength = 0;
for (byte i = 0; i < innerBitmapLengths.length; i++) {
if ((innerBitmap & (1 << i)) != 0) {
innerDataLength += innerBitmapLengths[i];
}
}
if (this.rawHeader.length != 14)
throw new LengthMismatchException("Workout data header length mismatch.");
this.header = new Header();
ByteBuffer buf = ByteBuffer.wrap(this.rawHeader);
header.workoutNumber = buf.getShort();
header.dataNumber = buf.getShort();
header.timestamp = buf.getInt();
header.interval = buf.get();
header.dataCount = buf.getShort();
header.dataLength = buf.get();
header.bitmap = buf.getShort();
// Check data lengths from header
if (this.header.dataCount * this.header.dataLength != this.rawData.length)
throw new LengthMismatchException("Workout data length mismatch with header.");
// Check data lengths from bitmap
int dataLength = 0;
for (byte i = 0; i < bitmapLengths.length; i++) {
if ((header.bitmap & (1 << i)) != 0) {
if (i == 6) {
dataLength += innerDataLength;
} else {
dataLength += bitmapLengths[i];
}
}
}
dataLength = dataLength * header.dataCount;
if (dataLength != this.rawData.length)
throw new LengthMismatchException("Workout data length mismatch with bitmap.");
this.dataList = new ArrayList<>();
buf = ByteBuffer.wrap(this.rawData);
for (short i = 0; i < header.dataCount; i++) {
Data data = new Data();
data.timestamp = header.timestamp + header.interval * i;
for (byte j = 0; j < bitmapLengths.length; j++) {
if ((header.bitmap & (1 << j)) != 0) {
switch (j) {
case 0:
data.heartRate = buf.get();
break;
case 1:
data.speed = buf.getShort();
break;
case 2:
data.stepRate = buf.get();
break;
case 3:
data.swolf = buf.getShort();
break;
case 4:
data.strokeRate = buf.getShort();
break;
case 5:
data.altitude = buf.getInt();
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();
break;
default:
data.unknownData = this.tlv.serialize();
// Fix alignment
for (int l = 0; l < innerBitmapLengths[k]; l++)
buf.get();
break;
}
}
}
break;
case 7:
data.calories = buf.getShort();
break;
case 8:
data.frequency = buf.getShort();
break;
case 9:
data.cyclingPower = buf.getShort();
break;
default:
data.unknownData = this.tlv.serialize();
// Fix alignment
for (int k = 0; k < bitmapLengths[j]; k++)
buf.get();
break;
}
}
}
this.dataList.add(data);
}
}
}
}
public static class WorkoutPace {
public static final int id = 0x0c;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
short workoutNumber,
short paceNumber
) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV()
.put(0x02, workoutNumber)
.put(0x08, paceNumber)
);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public static class Block {
public short distance = -1;
public byte type = -1;
public int pace = -1;
public short pointIndex = 0;
public short correction = 0;
public boolean hasCorrection = false;
@Override
public String toString() {
return "Block{" +
"distance=" + distance +
", type=" + type +
", pace=" + pace +
", pointIndex=" + pointIndex +
", correction=" + correction +
", hasCorrection=" + hasCorrection +
'}';
}
}
public short workoutNumber;
public short paceNumber;
public List blocks;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
this.workoutNumber = container.getShort(0x02);
this.paceNumber = container.getShort(0x08);
this.blocks = new ArrayList<>();
for (HuaweiTLV blockTlv : container.getObjects(0x83)) {
Block block = new Block();
block.distance = blockTlv.getShort(0x04);
block.type = blockTlv.getByte(0x05);
block.pace = blockTlv.getInteger(0x06);
if (blockTlv.contains(0x07))
block.pointIndex = blockTlv.getShort(0x07);
if (blockTlv.contains(0x09)) {
block.hasCorrection = true;
block.correction = blockTlv.getShort(0x09);
}
blocks.add(block);
}
}
}
}
public static class WorkoutSwimSegments {
public static final int id = 0x0e;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
short workoutNumber,
short segmentNumber
) {
super(paramsProvider);
this.serviceId = Workout.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x81, new HuaweiTLV()
.put(0x02, workoutNumber)
.put(0x08, segmentNumber)
);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public static class Block {
public short distance = -1;
public byte type = -1;
public int pace = -1;
public short pointIndex = 0;
public short segment = -1;
public byte swimType= -1;
public short strokes = -1;
public short avgSwolf = -1;
public int time= -1;
@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();
}
}
public short workoutNumber;
public short segmentNumber;
public List blocks;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
this.workoutNumber = container.getShort(0x02);
this.segmentNumber = container.getShort(0x08);
this.blocks = new ArrayList<>();
for (HuaweiTLV blockTlv : container.getObjects(0x83)) {
Block block = new Block();
block.distance = blockTlv.getShort(0x04);
block.type = blockTlv.getByte(0x05);
block.pace = blockTlv.getInteger(0x06);
if (blockTlv.contains(0x07))
block.pointIndex = blockTlv.getShort(0x07);
if (blockTlv.contains(0x09))
block.segment = blockTlv.getShort(0x09);
if (blockTlv.contains(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);
blocks.add(block);
}
}
}
}
public static class NotifyHeartRate {
public static final int id = 0x17;
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, 0x03);
this.complete = true;
}
}
}
}