/* Copyright (C) 2024 Damien Gaignon, Martin.JM, Vitalii Tomin
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.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiReportThreshold;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiRunPaceConfig;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
public class FitnessData {
public static final byte id = 0x07;
public static class MotionGoal {
public static final byte id = 0x01;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider,
byte goalType,
byte frameType,
int stepGoal,
int calorieGoal,
short durationGoal) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
frameType = (frameType == 0x01) ? 0x01 : Type.motion;
HuaweiTLV subTlv = new HuaweiTLV()
.put(0x03, goalType)
.put(0x04, frameType);
stepGoal = ((Type.data & 0x01) != 0x00) ? stepGoal : 0xffffffff;
if (stepGoal != 0xffffffff)
subTlv.put(0x05, stepGoal);
int calorieGoalFinal = ((Type.data & 0x02) != 0x00) ? calorieGoal : 0xffffffff;
if (calorieGoalFinal != 0xffffffff) {
subTlv.put(0x06, calorieGoalFinal);
} else if (frameType == 0x01) {
subTlv.put(0x06, stepGoal / 0x1e);
}
int distanceGoal = ((Type.data & 0x04) != 0x00) ? durationGoal : 0xffffffff;
if (distanceGoal != 0xffffffff) {
subTlv.put(0x07, distanceGoal);
} else if (frameType == 0x01) {
subTlv.put(0x06, stepGoal);
}
short durationGoalFinal = ((Type.data & 0x08) != 0x00) ? durationGoal : 0xffffffff;
if (durationGoalFinal != 0xffffffff) {
subTlv.put(0x08, durationGoalFinal);
}
HuaweiTLV containerTlv = new HuaweiTLV().put(0x82, subTlv);
this.tlv = new HuaweiTLV()
.put(0x81, containerTlv);
}
}
}
public static class UserInfo {
public static final byte id = 0x02;
public static class UserInfoData {
public static final int GENDER_MALE = 1;
public static final int GENDER_FEMALE = 2;
public static final int GENDER_UNKNOWN = 3;
private static final float RUN_CF = 0.83f;
private static final float WALK_CF = 0.42f;
public static final int DEFAULT_HEIGHT = 175;
public static final int DEFAULT_WEIGHT = 65;
private final int height;
private final float weight;
private final int age;
private final int birthday;
private final byte gender;
private final boolean unknownGenderSupported;
private final boolean vo2Supported;
private final int vo2Max;
private final int vo2Time;
private final boolean precisionWeightSupported;
public UserInfoData(int height, float weight, int age, int birthday, byte gender, boolean unknownGenderSupported, boolean vo2Supported, int vo2Max, int vo2Time, boolean precisionWeightSupported) {
this.height = height;
this.weight = weight;
this.age = age;
this.birthday = birthday;
this.gender = gender;
this.unknownGenderSupported = unknownGenderSupported;
this.vo2Supported = vo2Supported;
this.vo2Max = vo2Max;
this.vo2Time = vo2Time;
this.precisionWeightSupported = precisionWeightSupported;
}
public int getHeight() {
if (this.height > 0)
return this.height;
return DEFAULT_HEIGHT;
}
public float getWeight() {
if (this.weight > 0)
return this.weight;
return DEFAULT_WEIGHT;
}
public int getAge() {
return age;
}
public int getBirthday() {
return birthday;
}
public byte getGender() {
return gender;
}
public boolean isUnknownGenderSupported() {
return unknownGenderSupported;
}
public boolean isVo2Supported() {
return vo2Supported;
}
public int getVo2Max() {
return vo2Max;
}
public int getVo2Time() {
return vo2Time;
}
public boolean isPrecisionWeightSupported() {
return precisionWeightSupported;
}
public boolean isGenderValid() {
return this.gender == GENDER_MALE || this.gender == GENDER_FEMALE || this.gender == GENDER_UNKNOWN;
}
public boolean isBirthdayValid() {
return this.birthday > 0;
}
}
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, UserInfoData info) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
int height = info.getHeight();
float weight = info.getWeight();
byte bmiWalk = (byte) Math.round(UserInfoData.WALK_CF * height);
byte bmiRun = (byte) Math.round(UserInfoData.RUN_CF * height);
this.tlv = new HuaweiTLV()
.put(0x01, (byte) height)
.put(0x02, (byte) weight)
.put(0x03, (byte) info.getAge());
if (info.isBirthdayValid())
this.tlv.put(0x04, info.getBirthday());
if (info.isGenderValid()) {
if (info.unknownGenderSupported || info.gender != UserInfoData.GENDER_UNKNOWN)
this.tlv.put(0x05, info.gender);
}
this.tlv.put(0x06, bmiWalk);
this.tlv.put(0x07, bmiRun);
if (info.vo2Supported) {
this.tlv.put(0x08, info.getVo2Max());
this.tlv.put(0x09, info.getVo2Time());
}
if (info.precisionWeightSupported) {
int precisionWeight = Math.round(weight * 100f);
this.tlv.put(0x0b, precisionWeight);
}
}
}
}
public static class MessageCount {
public static final byte sleepId = 0x0C;
public static final byte stepId = 0x0A;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
byte commandId,
int start,
int end
) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = commandId;
this.tlv = new HuaweiTLV()
.put(0x81)
.put(0x03, start)
.put(0x04, end);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public short count;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
this.count = this.tlv.getObject(0x81).getShort(0x02);
this.complete = true;
}
}
}
public static class MessageData {
public static final byte sleepId = 0x0D;
public static final byte stepId = 0x0B;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, byte commandId, short count) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = commandId;
this.tlv = new HuaweiTLV()
.put(0x81, new HuaweiTLV()
.put(0x02, count)
);
this.complete = true;
}
}
public static class SleepResponse extends HuaweiPacket {
public static class SubContainer {
public byte type;
public byte[] timestamp;
@Override
public String toString() {
return "SubContainer{" +
"type=" + type +
", timestamp=" + Arrays.toString(timestamp) +
'}';
}
}
public short number;
public List containers;
public SleepResponse(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = sleepId;
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
List subContainers = container.getObjects(0x83);
this.number = container.getShort(0x02);
this.containers = new ArrayList<>();
for (HuaweiTLV subContainerTlv : subContainers) {
SubContainer subContainer = new SubContainer();
subContainer.type = subContainerTlv.getByte(0x04);
subContainer.timestamp = subContainerTlv.getBytes(0x05);
this.containers.add(subContainer);
}
}
}
public static class StepResponse extends HuaweiPacket {
public static class SubContainer {
public static class TV {
public final byte bitmap;
public final byte tag;
public final short value;
public TV(byte bitmap, byte tag, short value) {
this.bitmap = bitmap;
this.tag = tag;
this.value = value;
}
@Override
public String toString() {
return "TV{" +
"bitmap=" + bitmap +
", tag=" + tag +
", value=" + value +
'}';
}
}
/*
* Data directly from packet
*/
public byte timestampOffset;
public byte[] data;
/*
* Inferred data
*/
public int timestamp;
public List parsedData = null;
public String parsedDataError = "";
public int steps = -1;
public int calories = -1;
public int distance = -1;
public int heartrate = -1;
public int spo = -1;
public List unknownTVs = null;
@Override
public String toString() {
return "SubContainer{" +
"timestampOffset=" + timestampOffset +
", data=" + Arrays.toString(data) +
", timestamp=" + timestamp +
", parsedData=" + parsedData +
", parsedDataError='" + parsedDataError + '\'' +
", steps=" + steps +
", calories=" + calories +
", distance=" + distance +
", spo=" + spo +
", unknownTVs=" + unknownTVs +
'}';
}
}
public short number;
public int timestamp;
public List containers;
private static final List singleByteTagListBitmap1 = new ArrayList<>();
static {
singleByteTagListBitmap1.add((byte) 0x20);
singleByteTagListBitmap1.add((byte) 0x40);
}
public StepResponse(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = stepId;
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
List subContainers = container.getObjects(0x84);
this.number = container.getShort(0x02);
this.timestamp = container.getInteger(0x03);
this.containers = new ArrayList<>();
for (HuaweiTLV subContainerTlv : subContainers) {
SubContainer subContainer = new SubContainer();
subContainer.timestampOffset = subContainerTlv.getByte(0x05);
subContainer.timestamp = this.timestamp + 60 * subContainer.timestampOffset;
subContainer.data = subContainerTlv.getBytes(0x06);
parseData(subContainer, subContainer.data);
this.containers.add(subContainer);
}
}
private static void parseData(SubContainer returnValue, byte[] data) {
int i = 0;
if (data.length <= 0) {
returnValue.parsedData = null;
returnValue.parsedDataError = "Data is missing feature bitmap.";
return;
}
byte featureBitmap1 = data[i++];
byte featureBitmap2 = 0;
if ((featureBitmap1 & 128) != 0) {
if (data.length <= i) {
returnValue.parsedData = null;
returnValue.parsedDataError = "Data is missing second feature bitmap.";
return;
}
featureBitmap2 = data[i++];
}
returnValue.parsedData = new ArrayList<>();
returnValue.unknownTVs = new ArrayList<>();
// The greater than zero check is because Java is always signed, so we only check 7 bits
for (byte bitToCheck = 1; bitToCheck > 0; bitToCheck <<= 1) {
if ((featureBitmap1 & bitToCheck) != 0) {
short value;
if (singleByteTagListBitmap1.contains(bitToCheck)) {
if (data.length - 1 < i) {
returnValue.parsedData = null;
returnValue.parsedDataError = "Data is too short for selected features.";
return;
}
value = data[i++];
} else {
if (data.length - 2 < i) {
returnValue.parsedData = null;
returnValue.parsedDataError = "Data is too short for selected features.";
return;
}
value = (short) ((data[i++] & 0xFF) << 8 | (data[i++] & 0xFF));
}
// The bitToCheck is used as tag, which may not be optimal, but works
SubContainer.TV tv = new SubContainer.TV((byte) 1, bitToCheck, value);
returnValue.parsedData.add(tv);
if (bitToCheck == 0x02)
returnValue.steps = value;
else if (bitToCheck == 0x04)
returnValue.calories = value;
else if (bitToCheck == 0x08)
returnValue.distance = value;
else if (bitToCheck == 0x40)
returnValue.heartrate = value;
else
returnValue.unknownTVs.add(tv);
}
}
if (featureBitmap2 != 0) {
// We want to check 8 bits here, and java is java, so we use a short
for (short bitToCheck = 1; bitToCheck < 0x0100; bitToCheck <<= 1) {
if ((featureBitmap2 & bitToCheck) != 0) {
if (data.length - 1 < i) {
returnValue.parsedData = null;
returnValue.parsedDataError = "Data is too short for selected features.";
return;
}
byte value = data[i++];
SubContainer.TV tv = new SubContainer.TV((byte) 2, (byte) bitToCheck, value);
returnValue.parsedData.add(tv);
if (bitToCheck == 0x01)
returnValue.spo = value;
else
returnValue.unknownTVs.add(tv);
}
}
}
}
}
}
public static class FitnessTotals {
public static final byte id = 0x03;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public int totalSteps = 0;
public int totalCalories = 0;
public int totalDistance = 0;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x81);
List containers = container.getObjects(0x83);
for (HuaweiTLV tlv : containers) {
if (tlv.contains(0x05))
totalSteps += tlv.getInteger(0x05);
if (tlv.contains(0x06))
totalCalories += tlv.getShort(0x06);
if (tlv.contains(0x07))
totalDistance += tlv.getInteger(0x07);
}
this.complete = true;
}
}
}
public static class ActivityReminder {
public static final byte id = 0x07;
public static class Request extends HuaweiPacket {
public Request(
ParamsProvider paramsProvider,
boolean longSitSwitch,
byte longSitInterval,
byte[] longSitStart,
byte[] longSitEnd,
byte cycle
) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x81, new HuaweiTLV()
.put(0x02, longSitSwitch)
.put(0x03, longSitInterval)
.put(0x04, longSitStart)
.put(0x05, longSitEnd)
.put(0x06, cycle)
);
this.complete = true;
}
}
}
public static class DeviceReportThreshold {
public static final byte id = 0xe;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, List thresholds) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
HuaweiTLV subTlv = new HuaweiTLV();
for (HuaweiReportThreshold th : thresholds) {
subTlv.put(0x02, th.getBytes());
}
this.tlv = new HuaweiTLV().put(0x81, subTlv);
this.complete = true;
}
}
}
public static class HeartRateZoneConfigPacket {
// It can use two IDs with basically the same format.
public static final byte id_simple = 0x13;
public static final byte id_extended = 0x21;
public static class Request extends HuaweiPacket {
private Request(
ParamsProvider paramsProvider,
byte id,
HeartRateZonesConfig heartRateZonesConfig
) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
HuaweiTLV subTlv = new HuaweiTLV().
put(0x08, heartRateZonesConfig.getWarningEnable());
if (
heartRateZonesConfig.hasValidMHRData() &&
heartRateZonesConfig.getWarningHRLimit() > 0 &&
heartRateZonesConfig.getMaxHRThreshold() > 0
) {
subTlv
.put(0x09, (byte) heartRateZonesConfig.getWarningHRLimit())
.put(0x02, (byte) heartRateZonesConfig.getMHRWarmUp())
.put(0x03, (byte) heartRateZonesConfig.getMHRFatBurning())
.put(0x04, (byte) heartRateZonesConfig.getMHRAerobic())
.put(0x05, (byte) heartRateZonesConfig.getMHRAnaerobic())
.put(0x06, (byte) heartRateZonesConfig.getMHRExtreme())
.put(0x07, (byte) heartRateZonesConfig.getMaxHRThreshold())
.put(0x0b, (byte) heartRateZonesConfig.getMaxHRThreshold());
}
if (id == id_extended && heartRateZonesConfig.hasValidHRRData()) {
subTlv
.put(0x0d, (byte) heartRateZonesConfig.getHRRBasicAerobic())
.put(0x0e, (byte) heartRateZonesConfig.getHRRAdvancedAerobic())
.put(0x0f, (byte) heartRateZonesConfig.getHRRLactate())
.put(0x10, (byte) heartRateZonesConfig.getHRRBasicAnaerobic())
.put(0x11, (byte) heartRateZonesConfig.getHRRAdvancedAnaerobic());
}
if (id == id_extended && heartRateZonesConfig.getRestHeartRate() > 0) {
subTlv
.put(0x0a, (byte) heartRateZonesConfig.getCalculateMethod())
.put(0x0c, (byte) heartRateZonesConfig.getRestHeartRate());
}
this.tlv = new HuaweiTLV().put(0x81, subTlv);
this.complete = true;
}
public static Request requestSimple(ParamsProvider paramsProvider, HeartRateZonesConfig heartRateZonesConfig) {
return new Request(paramsProvider, id_simple, heartRateZonesConfig);
}
public static Request requestExtended(ParamsProvider paramsProvider, HeartRateZonesConfig heartRateZonesConfig) {
return new Request(paramsProvider, id_extended, heartRateZonesConfig);
}
}
}
public static class TruSleep {
public static final byte id = 0x16;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, boolean truSleepSwitch) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, truSleepSwitch);
this.complete = true;
}
}
}
public static class EnableAutomaticHeartrate {
public static final byte id = 0x17;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, boolean enableAutomaticHeartrate) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, enableAutomaticHeartrate);
this.isEncrypted = true;
this.complete = true;
}
}
}
public static class NotifyRestHeartRate {
public static final byte id = 0x23;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, 0x01);
this.complete = true;
}
}
}
public static class EnableAutomaticSpo {
public static final byte id = 0x24;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, boolean enableAutomaticSpo) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, enableAutomaticSpo);
this.isEncrypted = true;
this.complete = true;
}
}
}
public static class RunPaceConfig {
public static final byte id = 0x28;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, final HuaweiRunPaceConfig runPaceConfig) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, (short) runPaceConfig.getZone1JogMin())
.put(0x02, (short) runPaceConfig.getZone2MarathonMin())
.put(0x03, (short) runPaceConfig.getZone3LactateThresholdMin())
.put(0x04, (short) runPaceConfig.getZone4AnaerobicMin())
.put(0x05, (short) runPaceConfig.getZone5HIITRunMin())
.put(0x06, (short) runPaceConfig.getZone5HIITRunMax());
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public boolean isOk;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
}
@Override
public void parseTlv() throws ParseException {
isOk = this.tlv.getInteger(0x7f) == 0x000186A0;
}
}
}
public static class MediumToStrengthThreshold {
public static final byte id = 0x29;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider,
byte walkRun,
byte climb,
byte heartRate,
byte cycleSpeed,
byte sample,
byte countLength,
int walkRunSpeed,
int walkRunWithHeartRate) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
if (walkRun < 0x00 || walkRun > 0xc8) walkRun = 0x6E;
if (climb < 0x0 || climb > 0xc8) climb = 0x3c;
if (heartRate < 0x0 || heartRate > 0x64) heartRate = 0x40;
if (cycleSpeed < 0x0 || cycleSpeed > 0xff) cycleSpeed = 0x50;
if (sample < 0x1 || sample > 0xa) sample = 0x3;
if (countLength < 0x1 || countLength > 0xa) countLength = 0x5;
if (countLength < sample) countLength = sample;
this.tlv = new HuaweiTLV()
.put(0x01, walkRun)
.put(0x02, climb)
.put(0x03, heartRate)
.put(0x04, cycleSpeed)
.put(0x05, sample)
.put(0x06, countLength);
if (walkRunSpeed != -1) {
this.tlv.put(0x07, (byte) walkRunSpeed);
}
if (walkRunWithHeartRate != -1) {
this.tlv.put(0x08, (byte) walkRunWithHeartRate);
}
this.complete = true;
}
}
}
public static class SkinTemperatureMeasurement {
public static final byte id = 0x2a;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, boolean temperatureSwitch) {
super(paramsProvider);
this.serviceId = FitnessData.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, (byte) 0x01)
.put(0x02, temperatureSwitch);
this.complete = true;
}
}
}
public static class Type {
// TODO: enum?
public static final byte goal = 0x01;
public static final byte motion = 0x00;
public static final byte data = 0x01;
}
}