1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-25 01:55:50 +01:00

Huawei: Send default HR Zones config to the device using P2P service

This commit is contained in:
Me7c7 2024-10-26 20:09:45 +03:00
parent 27f61138bc
commit 5f822149bc
5 changed files with 487 additions and 0 deletions

View File

@ -0,0 +1,234 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
// TODO: make this configurable
// TODO: retrieve from device and set real Rest HR
// NOTE: algorithms used in this class are generic. So this data can be used with other devices.
// We can move this class to global scope.
public class HeartRateZonesConfig {
public static final int TYPE_UPRIGHT = 1;
public static final int TYPE_SITTING = 2;
public static final int TYPE_SWIMMING = 3;
public static final int TYPE_OTHER = 4;
private static final int DEFAULT_REST_HEART_RATE = 60;
public static final int MAXIMUM_HEART_RATE = 220;
private final int configType;
private int calculateMethod = 0;
private int maxHRThreshold;
private int restHeartRate = DEFAULT_REST_HEART_RATE;
private boolean warningEnable;
private int warningHRLimit;
//MHR percentage
private int MHRExtreme;
private int MHRAnaerobic;
private int MHRAerobic;
private int MHRFatBurning;
private int MHRWarmUp;
//HRR percentage
private int HRRAdvancedAnaerobic;
private int HRRBasicAnaerobic;
private int HRRLactate;
private int HRRAdvancedAerobic;
private int HRRBasicAerobic;
//LTHR percentage
private int LTHRAnaerobic;
private int LTHRLactate;
private int LTHRAdvancedAerobic;
private int LTHRBasicAerobic;
private int LTHRWarmUp;
private int LTHRThresholdHeartRate;
public HeartRateZonesConfig(int type, int age) {
this.configType = type;
this.warningEnable = true;
this.warningHRLimit = MAXIMUM_HEART_RATE - age;
resetHRZones(age);
}
public void resetHRZones(int age) {
this.maxHRThreshold = (MAXIMUM_HEART_RATE - age) - getHRCorrection();
resetHRRZonesConfig();
resetMHRZonesConfig();
//NOTE: LTHR only supported for TYPE_UPRIGHT
if (this.configType == TYPE_UPRIGHT) {
resetLTHRZonesConfig();
}
}
private void resetLTHRZonesConfig() {
this.LTHRThresholdHeartRate = Math.round(((float) this.restHeartRate) + (((float) ((this.maxHRThreshold - this.restHeartRate) * 85)) / 100.0f));
this.LTHRAnaerobic = Math.round(((float) (this.LTHRThresholdHeartRate * 102)) / 100.0f);
this.LTHRLactate = Math.round(((float) (this.LTHRThresholdHeartRate * 97)) / 100.0f);
this.LTHRAdvancedAerobic = Math.round(((float) (this.LTHRThresholdHeartRate * 89)) / 100.0f);
this.LTHRBasicAerobic = Math.round(((float) (this.LTHRThresholdHeartRate * 80)) / 100.0f);
this.LTHRWarmUp = Math.round(((float) (this.LTHRThresholdHeartRate * 67)) / 100.0f);
}
private void resetMHRZonesConfig() {
this.MHRExtreme = Math.round(((float) (this.maxHRThreshold * 90)) / 100.0f);
this.MHRAnaerobic = Math.round(((float) (this.maxHRThreshold * 80)) / 100.0f);
this.MHRAerobic = Math.round(((float) (this.maxHRThreshold * 70)) / 100.0f);
this.MHRFatBurning = Math.round(((float) (this.maxHRThreshold * 60)) / 100.0f);
this.MHRWarmUp = Math.round(((float) (this.maxHRThreshold * 50)) / 100.0f);
}
private void resetHRRZonesConfig() {
int calcHR = this.maxHRThreshold - this.restHeartRate;
this.HRRAdvancedAnaerobic = Math.round(((float) (calcHR * 95)) / 100.0f) + this.restHeartRate;
this.HRRBasicAnaerobic = Math.round(((float) (calcHR * 88)) / 100.0f) + this.restHeartRate;
this.HRRLactate = Math.round(((float) (calcHR * 84)) / 100.0f) + this.restHeartRate;
this.HRRAdvancedAerobic = Math.round(((float) (calcHR * 74)) / 100.0f) + this.restHeartRate;
this.HRRBasicAerobic = Math.round(((float) (calcHR * 59)) / 100.0f) + this.restHeartRate;
}
//TODO: I am not sure about this. But it looks correct.
private int getHRCorrection() {
switch (this.configType) {
case TYPE_SITTING:
return 6;
case TYPE_SWIMMING:
return 10;
case TYPE_OTHER:
return 5;
default:
return 0;
}
}
public int getCalculateMethod() {
return this.calculateMethod;
}
public boolean getWarningEnable() {
return this.warningEnable;
}
public int getWarningHRLimit() {
return this.warningHRLimit;
}
public int getMaxHRThreshold() {
return this.maxHRThreshold;
}
public int getRestHeartRate() {
return this.restHeartRate;
}
public int getMHRWarmUp() {
return this.MHRWarmUp;
}
public int getMHRFatBurning() {
return this.MHRFatBurning;
}
public int getMHRAerobic() {
return this.MHRAerobic;
}
public int getMHRAnaerobic() {
return this.MHRAnaerobic;
}
public int getMHRExtreme() {
return this.MHRExtreme;
}
public int getHRRBasicAerobic() {
return this.HRRBasicAerobic;
}
public int getHRRAdvancedAerobic() {
return this.HRRAdvancedAerobic;
}
public int getHRRLactate() {
return this.HRRLactate;
}
public int getHRRBasicAnaerobic() {
return this.HRRBasicAnaerobic;
}
public int getHRRAdvancedAnaerobic() {
return this.HRRAdvancedAnaerobic;
}
public int getLTHRThresholdHeartRate() {
return this.LTHRThresholdHeartRate;
}
public int getLTHRAnaerobic() {
return this.LTHRAnaerobic;
}
public int getLTHRLactate() {
return this.LTHRLactate;
}
public int getLTHRAdvancedAerobic() {
return this.LTHRAdvancedAerobic;
}
public int getLTHRBasicAerobic() {
return this.LTHRBasicAerobic;
}
public int getLTHRWarmUp() {
return this.LTHRWarmUp;
}
private boolean checkValue(int val) {
return val >= 0 && val < MAXIMUM_HEART_RATE;
}
public boolean isValid() {
return checkValue(this.configType) &&
checkValue(this.calculateMethod) &&
checkValue(this.warningHRLimit) &&
checkValue(this.maxHRThreshold) &&
checkValue(this.restHeartRate) &&
checkValue(this.MHRWarmUp) &&
checkValue(this.MHRFatBurning) &&
checkValue(this.MHRAerobic) &&
checkValue(this.MHRAnaerobic) &&
checkValue(this.MHRExtreme) &&
checkValue(this.HRRBasicAerobic) &&
checkValue(this.HRRAdvancedAerobic) &&
checkValue(this.HRRLactate) &&
checkValue(this.HRRBasicAnaerobic) &&
checkValue(this.HRRAdvancedAnaerobic) &&
checkValue(this.LTHRThresholdHeartRate) &&
checkValue(this.LTHRAnaerobic) &&
checkValue(this.LTHRLactate) &&
checkValue(this.LTHRAdvancedAerobic) &&
checkValue(this.LTHRBasicAerobic) &&
checkValue(this.LTHRWarmUp) &&
this.warningHRLimit > 0;
}
public boolean hasValidMHRData() {
return MHRWarmUp > 0 && MHRFatBurning > 0 && MHRAerobic > 0 && MHRAnaerobic > 0 && MHRExtreme > 0 && maxHRThreshold > 0;
}
public boolean hasValidHRRData() {
return restHeartRate > 0 && HRRBasicAerobic > 0 && HRRAdvancedAerobic > 0 && HRRLactate > 0 && HRRBasicAnaerobic > 0 && HRRAdvancedAnaerobic > 0;
}
public boolean hasValidLTHRData() {
return LTHRThresholdHeartRate > 0 && LTHRAnaerobic > 0 && LTHRLactate > 0 && LTHRAdvancedAerobic > 0 && LTHRBasicAerobic > 0 && LTHRWarmUp > 0;
}
}

View File

@ -583,6 +583,14 @@ public class HuaweiCoordinator {
return false;
}
public boolean supportsTrack() {
if (supportsExpandCapability())
return supportsExpandCapability(0x36);
return false;
}
public boolean supportsCalendar() {
if (supportsExpandCapability())
return supportsExpandCapability(171) || supportsExpandCapability(184);

View File

@ -0,0 +1,131 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
public class HuaweiSportHRZones {
private final HeartRateZonesConfig otherHRZonesConfig;
private final HeartRateZonesConfig SittingHRZonesConfig;
private final HeartRateZonesConfig UprightHRZonesConfig;
private final HeartRateZonesConfig SwimmingHRZonesConfig;
public HuaweiSportHRZones(int age) {
this.UprightHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_UPRIGHT, age);
this.SittingHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_SITTING, age);
this.SwimmingHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_SWIMMING, age);
this.otherHRZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_OTHER, age);
}
public HeartRateZonesConfig getHRZonesConfigByType(int type) {
if (type == HeartRateZonesConfig.TYPE_SITTING) {
return this.SittingHRZonesConfig;
} else if (type == HeartRateZonesConfig.TYPE_SWIMMING) {
return this.SwimmingHRZonesConfig;
} else if (type == HeartRateZonesConfig.TYPE_OTHER) {
return this.otherHRZonesConfig;
}
return this.UprightHRZonesConfig;
}
public byte[] getHRZonesData() {
HeartRateZonesConfig uprightConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_UPRIGHT);
HeartRateZonesConfig sittingConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_SITTING);
HeartRateZonesConfig swimmingConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_SWIMMING);
HeartRateZonesConfig otherConfig = getHRZonesConfigByType(HeartRateZonesConfig.TYPE_OTHER);
if (!uprightConfig.isValid() || !sittingConfig.isValid() || !swimmingConfig.isValid() || !otherConfig.isValid()) {
return null;
}
HuaweiTLV tlv = new HuaweiTLV();
if (uprightConfig.hasValidMHRData()) {
tlv.put(0x2, (byte) uprightConfig.getMHRWarmUp())
.put(0x3, (byte) uprightConfig.getMHRFatBurning())
.put(0x4, (byte) uprightConfig.getMHRAerobic())
.put(0x5, (byte) uprightConfig.getMHRAnaerobic())
.put(0x6, (byte) uprightConfig.getMHRExtreme())
.put(0x7, (byte) uprightConfig.getMaxHRThreshold())
.put(0x8, (byte) (uprightConfig.getWarningEnable() ? 1 : 0))
.put(0x9, (byte) uprightConfig.getWarningHRLimit())
.put(0xa, (byte) uprightConfig.getCalculateMethod())
.put(0xb, (byte) uprightConfig.getMaxHRThreshold());
}
tlv.put(0xc, (byte) uprightConfig.getRestHeartRate());
if (uprightConfig.hasValidHRRData()) {
tlv.put(0xd, (byte) uprightConfig.getHRRBasicAerobic())
.put(0xe, (byte) uprightConfig.getHRRAdvancedAerobic())
.put(0xf, (byte) uprightConfig.getHRRLactate())
.put(0x10, (byte) uprightConfig.getHRRBasicAnaerobic())
.put(0x11, (byte) uprightConfig.getHRRAdvancedAnaerobic());
}
if (uprightConfig.hasValidLTHRData()) {
tlv.put(0x3f, (byte) uprightConfig.getLTHRThresholdHeartRate())
.put(0x40, (byte) uprightConfig.getLTHRAnaerobic())
.put(0x41, (byte) uprightConfig.getLTHRLactate())
.put(0x42, (byte) uprightConfig.getLTHRAdvancedAerobic())
.put(0x43, (byte) uprightConfig.getLTHRBasicAerobic())
.put(0x44, (byte) uprightConfig.getLTHRWarmUp());
}
if (sittingConfig.hasValidMHRData()) {
tlv.put(0x12, (byte) (sittingConfig.getWarningEnable() ? 1 : 0))
.put(0x13, (byte) sittingConfig.getCalculateMethod())
.put(0x14, (byte) sittingConfig.getWarningHRLimit())
.put(0x15, (byte) sittingConfig.getMHRWarmUp())
.put(0x16, (byte) sittingConfig.getMHRFatBurning())
.put(0x17, (byte) sittingConfig.getMHRAerobic())
.put(0x18, (byte) sittingConfig.getMHRAnaerobic())
.put(0x19, (byte) sittingConfig.getMHRExtreme())
.put(0x1a, (byte) sittingConfig.getMaxHRThreshold());
}
if (sittingConfig.hasValidHRRData()) {
tlv.put(0x1b, (byte) sittingConfig.getRestHeartRate())
.put(0x1c, (byte) sittingConfig.getHRRBasicAerobic())
.put(0x1d, (byte) sittingConfig.getHRRAdvancedAerobic())
.put(0x1e, (byte) sittingConfig.getHRRLactate())
.put(0x1f, (byte) sittingConfig.getHRRBasicAnaerobic())
.put(0x20, (byte) sittingConfig.getHRRAdvancedAnaerobic());
}
if (swimmingConfig.hasValidMHRData()) {
tlv.put(0x21, (byte) (swimmingConfig.getWarningEnable() ? 1 : 0))
.put(0x22, (byte) swimmingConfig.getCalculateMethod())
.put(0x23, (byte) swimmingConfig.getWarningHRLimit())
.put(0x24, (byte) swimmingConfig.getMHRWarmUp())
.put(0x25, (byte) swimmingConfig.getMHRFatBurning())
.put(0x26, (byte) swimmingConfig.getMHRAerobic())
.put(0x27, (byte) swimmingConfig.getMHRAnaerobic())
.put(0x28, (byte) swimmingConfig.getMHRExtreme())
.put(0x29, (byte) swimmingConfig.getMaxHRThreshold());
}
if (swimmingConfig.hasValidHRRData()) {
tlv.put(0x2a, (byte) swimmingConfig.getRestHeartRate())
.put(0x2b, (byte) swimmingConfig.getHRRBasicAerobic())
.put(0x2c, (byte) swimmingConfig.getHRRAdvancedAerobic())
.put(0x2d, (byte) swimmingConfig.getHRRLactate())
.put(0x2e, (byte) swimmingConfig.getHRRBasicAnaerobic())
.put(0x2f, (byte) swimmingConfig.getHRRAdvancedAnaerobic());
}
if (otherConfig.hasValidMHRData()) {
tlv.put(0x30, (byte) (otherConfig.getWarningEnable() ? 1 : 0))
.put(0x31, (byte) otherConfig.getCalculateMethod())
.put(0x32, (byte) otherConfig.getWarningHRLimit())
.put(0x33, (byte) otherConfig.getMHRWarmUp())
.put(0x34, (byte) otherConfig.getMHRFatBurning())
.put(0x35, (byte) otherConfig.getMHRAerobic())
.put(0x36, (byte) otherConfig.getMHRAnaerobic())
.put(0x37, (byte) otherConfig.getMHRExtreme())
.put(0x38, (byte) otherConfig.getMaxHRThreshold());
}
if (otherConfig.hasValidHRRData()) {
tlv.put(0x39, (byte) otherConfig.getRestHeartRate())
.put(0x3a, (byte) otherConfig.getHRRBasicAerobic())
.put(0x3b, (byte) otherConfig.getHRRAdvancedAerobic())
.put(0x3c, (byte) otherConfig.getHRRLactate())
.put(0x3d, (byte) otherConfig.getHRRBasicAnaerobic())
.put(0x3e, (byte) otherConfig.getHRRAdvancedAnaerobic());
}
return tlv.serialize();
}
}

View File

@ -104,6 +104,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PCalendarService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p.HuaweiP2PTrackService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.AcceptAgreementsRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetAppInfoParams;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.GetContactsCount;
@ -880,6 +881,13 @@ public class HuaweiSupportProvider {
calendarService.register();
}
}
if (getHuaweiCoordinator().supportsTrack()) {
if (HuaweiP2PTrackService.getRegisteredInstance(huaweiP2PManager) == null) {
HuaweiP2PTrackService trackService = new HuaweiP2PTrackService(huaweiP2PManager);
trackService.register();
}
}
}
}
});

View File

@ -0,0 +1,106 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.p2p;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSportHRZones;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
public class HuaweiP2PTrackService extends HuaweiBaseP2PService {
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PTrackService.class);
public static final String MODULE = "hw.unitedevice.track";
private static final int HEADER_LENGTH = 36;
static class Sequence {
int counter;
private Sequence() {
this.counter = 0;
}
public int getNext() {
synchronized (this) {
this.counter = (this.counter + 1) % 10000;
return this.counter;
}
}
}
private final Sequence counter = new Sequence();
public HuaweiP2PTrackService(HuaweiP2PManager manager) {
super(manager);
LOG.info("HuaweiP2PTrackService");
}
@Override
public String getModule() {
return HuaweiP2PTrackService.MODULE;
}
@Override
public String getPackage() {
return "hw.watch.health.p2p";
}
@Override
public String getFingerprint() {
return "SystemApp";
}
public void sendHeartZoneConfig() {
ActivityUser activityUser = new ActivityUser();
HuaweiSportHRZones hrZones = new HuaweiSportHRZones(activityUser.getAge());
byte[] data = hrZones.getHRZonesData();
if (data == null) {
LOG.error("Incorrect Heart Rate config");
return;
}
ByteBuffer header = ByteBuffer.allocate(HEADER_LENGTH);
header.order(ByteOrder.LITTLE_ENDIAN);
header.putInt(2); // session id ??
header.putInt(1); // version
header.putInt(HEADER_LENGTH + data.length); // total length
header.putInt(0); // unknown, sub header length??
header.putInt(counter.getNext()); // message id
header.flip();
ByteBuffer packet = ByteBuffer.allocate(36 + data.length);
packet.put(header.array());
packet.put(data);
packet.flip();
LOG.info("HuaweiP2PTrackService sendHeartZoneConfig");
sendCommand(packet.array(), null);
}
@Override
public void registered() {
sendHeartZoneConfig();
}
@Override
public void unregister() {
}
@Override
public void handleData(byte[] data) {
LOG.info("HuaweiP2PTrackService handleData: {}", data.length);
}
public static HuaweiP2PTrackService getRegisteredInstance(HuaweiP2PManager manager) {
return (HuaweiP2PTrackService) manager.getRegisteredService(HuaweiP2PTrackService.MODULE);
}
}