diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HeartRateZonesConfig.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HeartRateZonesConfig.java new file mode 100644 index 000000000..6211b3c7b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HeartRateZonesConfig.java @@ -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; + } + +} 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 929f0222c..f70a5d603 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 @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSportHRZones.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSportHRZones.java new file mode 100644 index 000000000..af0a83f7d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huawei/HuaweiSportHRZones.java @@ -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(); + } + + +} 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 edbd15935..0489cb5c8 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 @@ -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(); + } + } + } } }); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PTrackService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PTrackService.java new file mode 100644 index 000000000..e8bc0bf80 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/p2p/HuaweiP2PTrackService.java @@ -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); + } +}