Huawei: Basic stress support. Calibration and enabling

This commit is contained in:
Me7c7
2025-03-28 23:19:52 +02:00
parent ced7fc5b98
commit 55df3c7aa5
26 changed files with 2189 additions and 18 deletions
+4
View File
@@ -209,6 +209,10 @@
android:name=".devices.garmin.GarminRealtimeSettingsActivity"
android:label="@string/loading"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity
android:name=".devices.huawei.ui.HuaweiStressCalibrationActivity"
android:label="@string/huawei_stress_calibrate"
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
<activity
android:name=".activities.ActivitySummariesActivity"
android:label="@string/activity_summaries"
@@ -99,6 +99,11 @@ public final class HuaweiConstants {
public static final String PREF_HUAWEI_HEART_RATE_LOW_ALERT = "pref_huawei_heart_rate_low_alert";
public static final String PREF_HUAWEI_HEART_RATE_HIGH_ALERT= "pref_huawei_heart_rate_high_alert";
public static final String PREF_HUAWEI_SPO_LOW_ALERT= "pref_huawei_spo_low_alert";
public static final String PREF_HUAWEI_STRESS_SWITCH= "pref_huawei_stress_switch";
public static final String PREF_HUAWEI_STRESS_CALIBRATE= "pref_huawei_stress_calibrate";
public static final String PREF_HUAWEI_STRESS_LAST_DATA= "huawei_stress_last_data";
public static final String PKG_NAME = "com.huawei.devicegroupmanage";
}
@@ -73,6 +73,7 @@ public class HuaweiCoordinator {
ByteBuffer notificationConstraints = null;
private boolean supportsTruSleepNewSync = false;
private boolean supportsRriNewSync = false;
private boolean supportsGpsNewSync = false;
private boolean supportsWorkoutNewSteps = false;
@@ -301,6 +302,10 @@ public class HuaweiCoordinator {
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.HEALTH, R.xml.devicesettings_temperature_automatic_enable);
}
if(supportsAutoStress()) {
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.HEALTH, R.xml.devicesettings_huawei_stress);
}
// Notifications
final List<Integer> notifications = deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.NOTIFICATIONS);
notifications.add(R.xml.devicesettings_notifications_enable);
@@ -681,6 +686,10 @@ public class HuaweiCoordinator {
return supportsCommandForService(0x01, 0x21);
}
public boolean supportsAutoStress() {
return supportsCommandForService(0x20, 0x09);
}
public boolean supportsExternalCalendarService() {
if (supportsExpandCapability())
return supportsExpandCapability(184);
@@ -1005,6 +1014,14 @@ public class HuaweiCoordinator {
this.supportsTruSleepNewSync = supportsTruSleepNewSync;
}
public boolean getSupportsRriNewSync() {
return supportsRriNewSync;
}
public void setSupportsRriNewSync(boolean supportsRriNewSync) {
this.supportsRriNewSync = supportsRriNewSync;
}
public boolean isSupportsGpsNewSync() {
return supportsGpsNewSync;
}
@@ -43,6 +43,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.EphemerisFile
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService0A;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FileDownloadService2C;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.HrRriTest;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.OTA;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.P2P;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Watchface;
@@ -608,6 +609,14 @@ public class HuaweiPacket {
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
}
case HrRriTest.id:
switch (this.commandId) {
case HrRriTest.RriData.id:
return new HrRriTest.RriData.Response(paramsProvider).fromPacket(this);
default:
this.isEncrypted = this.attemptDecrypt(); // Helps with debugging
return this;
}
case MusicControl.id:
switch (this.commandId) {
case MusicControl.MusicStatusResponse.id:
@@ -16,10 +16,13 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Parcel;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.SwitchPreferenceCompat;
@@ -28,8 +31,11 @@ import java.util.Set;
import java.util.Collections;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.AppBlacklistActivity;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminRealtimeSettingsActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui.HuaweiStressCalibrationActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@@ -43,6 +49,8 @@ import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstant
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_LOW_ALERT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_REALTIME_MODE;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_SPO_LOW_ALERT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_STRESS_CALIBRATE;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_TRUSLEEP;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_WORKMODE;
@@ -114,6 +122,9 @@ public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomize
handler.addPreferenceHandlerFor(PREF_HUAWEI_HEART_RATE_LOW_ALERT);
handler.addPreferenceHandlerFor(PREF_HUAWEI_HEART_RATE_HIGH_ALERT);
handler.addPreferenceHandlerFor(PREF_HUAWEI_SPO_LOW_ALERT);
handler.addPreferenceHandlerFor(PREF_HUAWEI_STRESS_SWITCH);
handler.addPreferenceHandlerFor(PREF_HUAWEI_STRESS_CALIBRATE);
final Preference forceOptions = handler.findPreference(PREF_FORCE_OPTIONS);
if (forceOptions != null) {
@@ -138,6 +149,16 @@ public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomize
if (this.coordinator.supportsWorkouts())
reparseWorkout.setVisible(true);
}
final Preference stressCalibrate = handler.findPreference("pref_huawei_stress_perform_calibrate");
if (stressCalibrate != null) {
stressCalibrate.setOnPreferenceClickListener(preference -> {
final Intent intent = new Intent(handler.getContext(), HuaweiStressCalibrationActivity.class);
intent.putExtra(GBDevice.EXTRA_DEVICE, handler.getDevice());
handler.getContext().startActivity(intent);
return true;
});
}
}
@Override
@@ -0,0 +1,193 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
public class HuaweiStressParser {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressParser.class);
public static class StressData {
public long startTime;
public long endTime;
public int algorithm;
public byte score;
public float scoreFactor;
public byte level;
public byte cFlag;
public byte type;
public byte accFlag;
public byte ppgFlag;
public List<Float> features = new ArrayList<>();
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("StressData{");
sb.append("startTime=").append(startTime);
sb.append(", endTime=").append(endTime);
sb.append(", algorithm=").append(algorithm);
sb.append(", score=").append(score);
sb.append(", scoreFactor=").append(scoreFactor);
sb.append(", level=").append(level);
sb.append(", cFlag=").append(cFlag);
sb.append(", type=").append(type);
sb.append(", accFlag=").append(accFlag);
sb.append(", ppgFlag=").append(ppgFlag);
sb.append(", features=").append(features);
sb.append('}');
return sb.toString();
}
}
public static class RriFileData {
public long fileSize;
public int version;
public int bitmap;
public List<StressData> stressData;
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("RriFileData{");
sb.append("fileSize=").append(fileSize);
sb.append(", version=").append(version);
sb.append(", bitmap=").append(bitmap);
sb.append(", stressData=").append(stressData);
sb.append('}');
return sb.toString();
}
}
public static String stressDataToJsonStr(StressData data) {
JSONObject obj = new JSONObject();
try {
obj.put("startTime", data.startTime);
obj.put("endTime", data.endTime);
obj.put("algorithm", data.algorithm);
obj.put("score", data.score);
obj.put("scoreFactor", data.scoreFactor);
obj.put("level", data.level);
obj.put("cFlag", data.cFlag);
obj.put("type", data.type);
obj.put("accFlag", data.accFlag);
obj.put("ppgFlag", data.ppgFlag);
JSONArray arr = new JSONArray();
for(Float f: data.features) {
arr.put(f);
}
obj.put("features", arr);
return obj.toString();
} catch (JSONException e) {
return null;
}
}
public static StressData stressDataFromJsonStr(String jsonStr) {
StressData st = new StressData();
try {
JSONObject obj = new JSONObject(jsonStr);
st.startTime = obj.getLong("startTime");
st.endTime = obj.getLong("endTime");
st.algorithm = obj.getInt("algorithm");
st.score = (byte) obj.getInt("score");
st.scoreFactor = (byte) obj.getInt("scoreFactor");
st.level = (byte) obj.getInt("level");
st.cFlag = (byte) obj.getInt("cFlag");
st.type = (byte) obj.getInt("type");
st.accFlag = (byte) obj.getInt("accFlag");
st.ppgFlag = (byte) obj.getInt("ppgFlag");
JSONArray arr = obj.getJSONArray("features");
for(int i = 0; i < arr.length(); i++) {
st.features.add((float) arr.getDouble(i));
}
} catch (JSONException e) {
return null;
}
if(st.endTime == 0 || st.score == 0 || st.features.size() != 12) {
return null;
}
return st;
}
public static RriFileData parseRri(byte[] rriData) {
if (rriData == null || rriData.length < 48) {
LOG.error("stress data is invalid");
return null;
}
ByteBuffer buffer = ByteBuffer.wrap(rriData);
buffer.order(ByteOrder.BIG_ENDIAN);
RriFileData fileData = new RriFileData();
fileData.fileSize = buffer.getLong();
fileData.bitmap = buffer.getShort();
fileData.version = buffer.getShort();
LOG.info("configFileSize :{}", fileData.fileSize);
LOG.info("configBitmap :{}", fileData.bitmap);
LOG.info("configVersion :{}", fileData.version);
// TODO: I don't have files with other version. Implement when discovered.
if (fileData.version == 3) {
// Skip unknown or reserved data
buffer.position(48);
fileData.stressData = new ArrayList<>();
// Each entry 66 bytes.
while (buffer.remaining() >= 66) {
ByteBuffer entryBuffer = ByteBuffer.allocate(66);
buffer.get(entryBuffer.array());
StressData stressData = new StressData();
int startTime = entryBuffer.getInt();
if (startTime == 0) {
LOG.warn("startTime == 0, skip ??");
continue;
}
stressData.startTime = startTime * 1000L;
int endTime = entryBuffer.getInt();
if (endTime == 0) {
LOG.warn("endTime == 0, skip ??");
continue;
}
stressData.endTime = endTime * 1000L;
stressData.algorithm = entryBuffer.getInt();
stressData.score = entryBuffer.get();
stressData.level = entryBuffer.get();
stressData.cFlag = entryBuffer.get();
stressData.type = entryBuffer.get();
stressData.accFlag = entryBuffer.get();
stressData.ppgFlag = entryBuffer.get();
for (int i = 0; i < 12; i++) {
stressData.features.add(entryBuffer.getFloat());
}
fileData.stressData.add(stressData);
}
// Sort data by end time. We need to save latest data for sending to device. Not sure.
if (fileData.stressData != null && !fileData.stressData.isEmpty()) {
fileData.stressData.sort((a, b) -> {
if (a != null && b != null) {
return Long.compare(a.endTime, b.endTime);
}
return 0;
});
}
} else {
LOG.warn("Rri file: unknown version: {}", fileData.version);
}
LOG.info("fileData:{}", fileData);
return fileData;
}
}
@@ -1560,6 +1560,7 @@ public class DeviceConfig {
public static class Response extends HuaweiPacket {
public boolean truSleepNewSync = false;
public boolean rriNewSync = false;
public boolean gpsNewSync = false;
public Response(ParamsProvider paramsProvider) {
@@ -1578,6 +1579,7 @@ public class DeviceConfig {
// Tag 2 -> File support
byte value = this.tlv.getByte(0x02);
truSleepNewSync = (value & 2) != 0;
rriNewSync = (value & 4) != 0;
gpsNewSync = (value & 8) != 0;
}
@@ -90,6 +90,24 @@ public class FileDownloadService0A {
}
}
public static class RriFileRequest extends HuaweiPacket {
public RriFileRequest(ParamsProvider paramsProvider, int startTime, int endTime) {
super(paramsProvider);
this.serviceId = FileDownloadService0A.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x02, (byte) 0x04)
.put(0x8f, new HuaweiTLV()
.put(0x10, startTime)
.put(0x11, endTime)
);
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public String[] fileNames;
@@ -113,16 +131,19 @@ public class FileDownloadService0A {
public static final int id = 0x02;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, boolean truSleep) {
public Request(ParamsProvider paramsProvider, byte fileType) {
super(paramsProvider);
this.serviceId = FileDownloadService0A.id;
this.commandId = id;
if (truSleep)
this.tlv = new HuaweiTLV().put(0x06, (byte) 0x01);
else
this.tlv = new HuaweiTLV().put(0x06, (byte) 0x02).put(0x07, (byte) 0x03);
// NOTE: empty for debug files
if(fileType != 0x0) {
this.tlv = new HuaweiTLV().put(0x06, fileType);
if (fileType == 0x02)
this.tlv.put(0x07, (byte) 0x03);
}
this.complete = true;
}
@@ -214,7 +235,7 @@ public class FileDownloadService0A {
public static class Response extends HuaweiPacket {
public boolean isOk;
public String filename;
public int offset;
public int offset = 0;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
@@ -225,7 +246,8 @@ public class FileDownloadService0A {
if (isOk) {
if (this.tlv.contains(0x01))
filename = this.tlv.getString(0x01);
offset = this.tlv.getInteger(0x02);
if (this.tlv.contains(0x02))
offset = this.tlv.getInteger(0x02);
}
}
}
@@ -33,6 +33,7 @@ public class FileDownloadService2C {
public enum FileType {
SLEEP_STATE,
SLEEP_DATA,
RRI,
GPS,
UNKNOWN; // Never use this as input
@@ -42,6 +43,8 @@ public class FileDownloadService2C {
return (byte) 0x0e;
case SLEEP_DATA:
return (byte) 0x0f;
case RRI:
return (byte) 0x10;
case GPS:
return (byte) 0x11;
default:
@@ -55,6 +58,8 @@ public class FileDownloadService2C {
return FileType.SLEEP_STATE;
case 0x0f:
return FileType.SLEEP_DATA;
case 0x10:
return FileType.RRI;
case 0x11:
return FileType.GPS;
default:
@@ -0,0 +1,80 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
public class HrRriTest {
public static final byte id = 0x19;
public static class OpenOrClose {
public static final byte id = 0x01;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, byte type) {
super(paramsProvider);
this.serviceId = HrRriTest.id;
this.commandId = id;
this.tlv = new HuaweiTLV().put(0x1, type);
this.isEncrypted = true;
this.complete = true;
}
}
public static class Response extends HuaweiPacket {
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
}
}
}
public static class RriData {
public static final byte id = 0x05;
public static class Response extends HuaweiPacket {
public static class rriSqiData {
public short rri;
public byte sqi;
@Override
public String toString() {
return "SubContainer{" +
"rri=" + rri +
", sqi=" + sqi +
'}';
}
}
public List<rriSqiData> containers;
public Response(ParamsProvider paramsProvider) {
super(paramsProvider);
}
@Override
public void parseTlv() throws ParseException {
HuaweiTLV container = this.tlv.getObject(0x82);
List<HuaweiTLV> subContainers = container.getObjects(0x83);
this.containers = new ArrayList<>();
for (HuaweiTLV subContainerTlv : subContainers) {
rriSqiData rriSqi = new rriSqiData();
rriSqi.rri = subContainerTlv.getShort(0x04);
rriSqi.sqi = subContainerTlv.getByte(0x05);
this.containers.add(rriSqi);
}
}
}
}
}
@@ -0,0 +1,41 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
import java.nio.ByteBuffer;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
public class Stress {
public static final byte id = 0x20;
public static class AutomaticStress {
public static final byte id = 0x09;
public static class Request extends HuaweiPacket {
public Request(ParamsProvider paramsProvider, byte status, byte score, List<Float> feature, int time) {
super(paramsProvider);
this.serviceId = Stress.id;
this.commandId = id;
this.tlv = new HuaweiTLV()
.put(0x01, status);
if(feature != null && feature.size() == 12) {
this.tlv.put(0x02, score);
ByteBuffer feat = ByteBuffer.allocate(12 * 4);
for (Float f: feature) {
feat.putFloat(f);
//feat.putInt(Float.floatToIntBits(f));
}
this.tlv.put(0x03, feat.array());
this.tlv.put(0x04, time);
}
this.isEncrypted = true;
this.complete = true;
}
}
}
}
@@ -0,0 +1,811 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class HuaweiStressHRVCalculation {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressHRVCalculation.class);
/*
This file contain realization of mostly standard time-domain and frequency-domain HRV analysis methods.
With parameters and restrictions specific for Huawei devices.
*/
private int calculateAndRemoveOutliers(int[] rriData, byte[] outliers) {
int dataSum = 0;
int outlierCount = 0;
for (int i = 0; i < rriData.length; i++) {
if ((rriData[i] <= 400) || (rriData[i] >= 1400)) {
outliers[i] = 1;
outlierCount++;
} else {
dataSum = dataSum + rriData[i];
}
}
if (rriData.length == outlierCount) {
return -1;
}
int firstNotOutlierIndex = -1;
float averageValue = (float) dataSum / (float) (rriData.length - outlierCount);
for (int i = 0; i < rriData.length; i++) {
if (outliers[i] == 0) {
float currentRri = (float) rriData[i];
if (currentRri <= averageValue * 1.2 && currentRri >= averageValue * 0.8) {
if (firstNotOutlierIndex < 0) {
firstNotOutlierIndex = i;
}
} else {
outliers[i] = 1;
}
}
}
if (firstNotOutlierIndex < 0) {
return -1;
}
int prevValue = rriData[firstNotOutlierIndex];
for (int i = firstNotOutlierIndex + 1; i < rriData.length; i++) {
if (outliers[i] == 0) {
float currentRri = (float) rriData[i];
if (currentRri <= prevValue * 0.8 || currentRri >= prevValue * 1.2) {
outliers[i] = 1;
}
prevValue = rriData[i];
}
}
return firstNotOutlierIndex;
}
public static class TimeDomainData {
public short medianNNICount = 0;
public float sdHR = 0;
public short rangeNNI = 0;
public float rmsSD = 0;
public short medianNNI = 0;
public float normVar = 0;
public short rriCount = 0;
public float meanHR = 0;
public float pNN50 = 0;
public float sdNN = 0;
}
private TimeDomainData calculateTimeDomain(int[] rriData, int firstNotOutlierIndex, byte[] outliers) {
TimeDomainData result = new TimeDomainData();
int rriMax = rriData[firstNotOutlierIndex];
int rriMin = rriData[firstNotOutlierIndex];
int rri50Count = 0;
int rriCount = 0;
int rriDiffSum = 0;
int rriDiffSqrtSum = 0;
int rriSum = 0;
float hrSum = 0;
int previousValue = 0;
boolean isFirstValue = true;
for (int i = 0; i < rriData.length; i++) {
if ((outliers[i] == 0) && (rriData[i] != 0)) {
rriSum += rriData[i];
hrSum += (60000.0F / (float) rriData[i]);
rriMax = Math.max(rriData[i], rriMax);
rriMin = Math.min(rriData[i], rriMin);
if (!isFirstValue) {
rriCount++;
int diff = rriData[i] - previousValue;
rriDiffSum += diff;
if (Math.abs(diff) > 50) {
rri50Count++;
}
rriDiffSqrtSum += diff * diff;
}
previousValue = rriData[i];
isFirstValue = false;
}
}
if (rriCount == 0) {
LOG.error("rriCount is zero");
return null;
}
result.rriCount = (short) (rriCount + 1);
result.meanHR = (hrSum / (float) (rriCount + 1));
result.pNN50 = ((float) rri50Count / (float) rriCount) * 100.0F;
int rriMinBy20 = (rriMin / 20) * 20;
int rriMaxBy20 = (rriMax / 20) * 20;
int categoriesNumber = ((rriMaxBy20 - rriMinBy20) / 20) + 1;
if (categoriesNumber >= 50) {
return null;
}
int[] categoriesBy20 = new int[categoriesNumber + 1];
byte[] categoriesCount = new byte[categoriesNumber + 1];
for (int i = 0; i <= categoriesNumber; i++) {
categoriesBy20[i] = rriMinBy20 + (i * 20);
}
float hrDiffSqrtSum = 0.0F;
float rrDiffSqrtSum = 0.0F;
float avrRR = rriSum / (float) result.rriCount;
for (int i = 0; i < rriData.length; i++) {
if ((outliers[i] == 0) && (rriData[i] != 0)) {
float rrDiff = (float) rriData[i] - avrRR;
rrDiffSqrtSum += (rrDiff * rrDiff);
double hrDiff = (60000.0F / (double) rriData[i]) - result.meanHR;
hrDiffSqrtSum += (float) (hrDiff * hrDiff);
for (int j = 0; j < categoriesNumber; j++) {
if ((categoriesBy20[j] <= rriData[i]) && (rriData[i] < categoriesBy20[j] + 20)) {
categoriesCount[j]++;
break;
}
}
}
}
result.sdHR = (float) Math.sqrt(hrDiffSqrtSum / (float) (rriCount + 1));
result.sdNN = (float) Math.sqrt(rrDiffSqrtSum / (float) (rriCount + 1));
result.rmsSD = (float) Math.sqrt((float) rriDiffSqrtSum / (float) rriCount);
float avgDiffSqrt = (float) (rriDiffSum * rriDiffSum) / rriCount;
float varSum = (float) (Math.sqrt(((float) rriDiffSqrtSum - avgDiffSqrt) / (float) rriCount) / 1.4142135F); // Math.sqrt(2) = 1.4142135F
float varDiff = (rrDiffSqrtSum / (float) rriCount) * 2 - (varSum * varSum);
if ((float) Math.sqrt(varDiff) == 0.0) {
return null;
}
result.normVar = varSum / (float) Math.sqrt(varDiff);
int sectionSum = 0;
int sectionCount = 0;
int maxValue = 0;
for (int i = 0; i < categoriesNumber; i++) {
if (categoriesCount[i] == 0) {
continue;
}
if (categoriesCount[i] > maxValue) {
maxValue = categoriesCount[i];
sectionCount = 1;
sectionSum = i;
continue;
}
if (categoriesCount[i] == maxValue) {
sectionCount++;
sectionSum += i;
}
}
if (sectionCount == 0) {
return null;
}
int meanCategoriesIndex = (int) ((float) sectionSum / (float) sectionCount + 0.5);
result.medianNNICount = categoriesCount[meanCategoriesIndex];
result.medianNNI = (short) categoriesBy20[meanCategoriesIndex];
result.rangeNNI = (short) (rriMaxBy20 - rriMinBy20);
return result;
}
private int interpolateRriData(int[] rriData, int[] rriCumulativeSum, float baseValue, float[] rriResample, int maxSamples, byte[] outliers) {
float targetValue = 0;
float previousCumulative = 0;
float lastRri = 0;
int currIdx = 0;
int resampleIdx = 0;
for (int i = 0; i < rriData.length; i++) {
if (outliers[i] != 0) {
continue;
}
if (resampleIdx > 129) {
return 0;
}
if (currIdx == 0) {
rriResample[resampleIdx] = rriData[i];
targetValue = (baseValue + 0.5F);
previousCumulative = baseValue;
lastRri = (float) rriData[i];
resampleIdx++;
currIdx = 1;
} else {
float currentCumulative = (float) (rriCumulativeSum[i] / 1000.0);
float cumulativeDelta = currentCumulative - targetValue;
if (cumulativeDelta >= -0.0001) {
if (currentCumulative == previousCumulative) {
return 0;
}
rriResample[resampleIdx] = (lastRri +
(((float) rriData[i] - lastRri) / (currentCumulative - previousCumulative)) *
(targetValue - previousCumulative));
resampleIdx++;
currIdx++;
if (currIdx == maxSamples) {
break;
}
targetValue = baseValue + ((float) currIdx * 0.5F);
for (int j = currIdx; j < maxSamples; j++) {
if ((currentCumulative == previousCumulative) || (((currentCumulative - targetValue) < -0.0001)))
break;
if (resampleIdx > 129) {
return 0;
}
rriResample[resampleIdx] =
(lastRri + (((float) rriData[i] - lastRri) / (currentCumulative - previousCumulative)) *
(targetValue - previousCumulative));
resampleIdx++;
currIdx++;
if (currIdx == maxSamples) break;
targetValue = baseValue + ((float) currIdx * 0.5F);
}
lastRri = (float) rriData[i];
previousCumulative = currentCumulative;
if (currIdx == maxSamples) break;
} else if ((cumulativeDelta < -0.0001)) {
lastRri = rriData[i];
previousCumulative = currentCumulative;
}
}
}
return resampleIdx;
}
private boolean applyHanningWindow(float[] rriResample, int maxLimit, int length) {
if ((length >= maxLimit) || (length == 0) || (rriResample == null) || (maxLimit <= 128)) {
return false;
}
final int halfLength = length / 2;
if ((halfLength >= maxLimit)) {
return false;
}
final boolean isLengthEven = (length % 2) == 0;
if (((length > 255) && isLengthEven) || ((length > 256) && (!isLengthEven))) {
for (int n = 0; n < 128; n++) {
double angle = ((double) (n + 1) * 2.0 * Math.PI) / (double) (length + 1);
rriResample[n] = rriResample[n] * (float) ((1.0 - Math.cos(angle)) * 0.5);
}
return true;
}
if (halfLength != 0) {
int n2 = length - 1;
for (int n = 0; n < halfLength; n++) {
double angle = ((double) (n + 1) * 2.0 * Math.PI) / (double) (length + 1);
float window = (float) ((1.0 - Math.cos(angle)) * 0.5);
rriResample[n] = rriResample[n] * window;
if (n2 < 0) {
return true;
}
if (length < 129) {
rriResample[n2] = rriResample[n2] * window;
} else {
if ((length - 128) <= n) {
rriResample[n2] = rriResample[n2] * window;
}
}
n2--;
}
}
if (isLengthEven) {
return true;
}
int midIndex = ((length + 1) / 2) - 1;
if ((midIndex >= 0) && (midIndex < maxLimit)) {
double angle = ((double) (midIndex + 1) * 2.0 * Math.PI) / (double) (length + 1);
rriResample[midIndex] = (float) ((double) rriResample[midIndex] * (1.0 - Math.cos(angle)) * 0.5);
return true;
}
return false;
}
private int prepareFrequencyDomainData(int firstNotOutlierIndex, int[] rriData, int[] rriCumulativeSum, float[] rriResample, byte[] outliers) {
int baseSumValue = rriCumulativeSum[0];
if (firstNotOutlierIndex > 0) {
baseSumValue = rriCumulativeSum[firstNotOutlierIndex];
}
double timeSpan = (double) (rriCumulativeSum[rriCumulativeSum.length - 1] - baseSumValue) / 1000.0;
int resampleSize = interpolateRriData(rriData, rriCumulativeSum, (float) (baseSumValue / 1000.0), rriResample, (int) (timeSpan * 2) + 1, outliers);
if ((resampleSize == 0) || (resampleSize >= rriResample.length)) {
LOG.error("interpolateRriData error");
return 0;
}
// Detrending data
float sumValues = 0.0F;
int sumSqrtIdx = 0;
int sumIdx = 0;
float sumIdxValues = 0.0F;
for (int i = 0; i < resampleSize; i++) {
sumIdx += i;
sumSqrtIdx += (i * i);
sumValues += rriResample[i];
sumIdxValues += (i * rriResample[i]);
}
final float meanIndex = sumIdx / (float) resampleSize;
float varianceTerm = sumSqrtIdx - ((float) resampleSize * meanIndex * meanIndex);
if (varianceTerm == 0.0) {
LOG.error("varianceTerm is zero");
return 0;
}
final float meanValue = sumValues / (float) resampleSize;
final float slope = (sumIdxValues - ((float) resampleSize * meanIndex * meanValue)) / varianceTerm;
final float intercept = meanValue - (slope * meanIndex);
// Apply detrending to data
for (int i = 0; i < resampleSize; i++) {
rriResample[i] -= (intercept + slope * i);
}
if (!applyHanningWindow(rriResample, rriResample.length, resampleSize)) {
LOG.error("applyHanningWindow error");
return 0;
}
return resampleSize;
}
public static class FrequencyDomainData {
float freqStep;
byte lfCount;
byte vlfCount;
byte hfCount;
float totalPsd;
float peakPower;
float peakFrequency;
float[] vlfSamples = new float[10];
float[] hfSamples = new float[40];
float vlfPsd;
float hfPsd;
float lfPsd;
float lfHfVlfTotalPsd;
int vlfPeakIndex;
int hfPeakIndex;
}
private boolean transformComplexComponents(float[] fftData, int size, int stepSize) {
if ((fftData == null) || (size >= 129)) {
return false;
}
for (int i = 0; i < size; i += (stepSize * 2)) {
float rP1 = fftData[i * 2];
float iP1 = fftData[(i * 2) + (stepSize * 2)];
float rP2 = fftData[(i * 2) + 1];
float iP2 = fftData[(i * 2) + (stepSize * 2) + 1];
fftData[(i * 2) + (stepSize * 2)] = rP1 - iP1;
fftData[(i * 2) + (stepSize * 2) + 1] = rP2 - iP2;
fftData[i * 2] = rP1 + iP1;
fftData[(i * 2) + 1] = rP2 + iP2;
}
return true;
}
private boolean computeFFTStage(int stageIndex, int butterflyStep, float[] fftData, int maxIndex, float[] sinCos) {
if ((maxIndex >= 129) || (fftData == null)) {
return false;
}
int sinCosIdx = (stageIndex - 1) * 2;
for (int i = 1; i < butterflyStep; i++) {
for (int j = i; j < maxIndex; j += (butterflyStep * 2)) {
float iPCurr = fftData[(j * 2) + 1];
float iPNext = fftData[(j * 2) + (butterflyStep * 2) + 1];
float rPCurr = fftData[j * 2];
float rpNext = fftData[(j * 2) + (butterflyStep * 2)];
float rDiff = rPCurr - rpNext;
float iDiff = iPCurr - iPNext;
fftData[(j * 2) + (butterflyStep * 2)] = ((sinCos[sinCosIdx] * rDiff) - (sinCos[sinCosIdx + 1] * iDiff));
fftData[(j * 2) + (butterflyStep * 2) + 1] = (sinCos[sinCosIdx + 1] * rDiff) + (sinCos[sinCosIdx] * iDiff);
fftData[j * 2] = rPCurr + rpNext;
fftData[(j * 2) + 1] = iPCurr + iPNext;
}
sinCosIdx += (stageIndex * 2);
}
return true;
}
private void swapComplexComponents(float[] data, int indexA, int indexB) {
float tempR = data[indexB * 2];
float tempI = data[(indexB * 2) + 1];
data[indexB * 2] = data[indexA * 2];
data[(indexB * 2) + 1] = data[(indexA * 2) + 1];
data[indexA * 2] = tempR;
data[(indexA * 2) + 1] = tempI;
}
private boolean performFFT(float[] fftData, int currentSize) {
int halfSize = currentSize / 2;
int halfSize1 = halfSize - 1;
if (currentSize == 0 || fftData == null || (halfSize * 2) == 0) {
return false;
}
float[] sinCos = new float[halfSize1 * 2];
double x = (Math.PI / (double) halfSize);
float cosX = (float) Math.cos(x);
float sinX = -(float) Math.sin(x);
float cosCurr = cosX;
float sinCurr = sinX;
for (int i = 0; i < halfSize1; i++) {
sinCos[i * 2] = cosCurr;
sinCos[(i * 2) + 1] = sinCurr;
float tmp = cosCurr * sinX;
cosCurr = ((cosCurr * cosX) - (sinCurr * sinX));
sinCurr = (sinCurr * cosX) + tmp;
}
// FFT decomposition
int partitionSize = currentSize;
int butterflyStep = 1;
for (int stageCount = 0; stageCount < currentSize; stageCount++) {
partitionSize /= 2;
if (!transformComplexComponents(fftData, currentSize, partitionSize)) {
return false;
}
if (!computeFFTStage(butterflyStep, partitionSize, fftData, currentSize, sinCos)) {
return false;
}
if (partitionSize == 1) break;
butterflyStep *= 2;
}
// Bit-reversal
int swapBoundary = currentSize / 2;
int sIdx = 0;
for (int tIdx = 1; tIdx < currentSize - 1; tIdx++) {
int n1 = swapBoundary;
while (sIdx >= n1) {
sIdx = sIdx - n1;
n1 = n1 / 2;
}
sIdx = (n1 + sIdx);
if (sIdx > tIdx) {
swapComplexComponents(fftData, tIdx, sIdx);
}
}
return true;
}
private boolean calculateFFT(float[] rriResample, int length) {
final int maxLength = rriResample.length - 2;
float[] fftData = new float[maxLength * 2];
int curLen = Math.min(length, maxLength);
int j = 0;
for (int i = 0; i < (curLen * 2); i += 2) {
fftData[i] = rriResample[j]; // real part
fftData[i + 1] = 0.0F; // imagine part
j++;
}
if (!performFFT(fftData, maxLength)) {
LOG.error("performFFT error");
return false;
}
Arrays.fill(rriResample, 0, length, 0);
int maxHalfLength = (maxLength / 2) + 1;
if (maxHalfLength == 0) {
return false;
}
int n = 0;
for (int i = 0; i < maxHalfLength; i++) {
rriResample[i] = ((fftData[n + 1] * fftData[n + 1] + fftData[n] * fftData[n]) * 2) / ((float) maxLength * 0.5F);
n += 2;
}
rriResample[0] = (fftData[1] * fftData[1] + fftData[0] * fftData[0]) / ((float) maxLength * 0.5F);
int li = ((maxHalfLength - 1) * 2); //last index
rriResample[maxHalfLength - 1] = (fftData[li + 1] * fftData[li + 1] + fftData[li] * fftData[li]) / ((float) maxLength * 0.5F);
return true;
}
private boolean calculateFrequencyDomain(float[] rriResample, int length, FrequencyDomainData fdData) {
// from 0.0 to 1.0 with freqStep
fdData.freqStep = 0.015625F;
for (int n = 0; n < 65; n++) {
float currentFrequency = fdData.freqStep * n;
rriResample[n] = (float) (((rriResample[n] * 64.0) / (float) length) * 0.5);
fdData.totalPsd += fdData.freqStep * rriResample[n];
if ((fdData.vlfCount >= fdData.vlfSamples.length) || (fdData.hfCount >= fdData.hfSamples.length)) {
LOG.error("vlfCount or hfCount too big");
return false;
}
// VLF: 0.0 - 0.04 Hz
// LF: 0.04 - 0.15 Hz
// HF: 0.15 - 0.4 Hz
if (currentFrequency <= 0.4F) {
fdData.lfHfVlfTotalPsd += fdData.freqStep * rriResample[n];
if ((currentFrequency >= 0.0F) && (currentFrequency < 0.04F)) {
fdData.vlfPsd += fdData.freqStep * rriResample[n];
fdData.vlfSamples[fdData.vlfCount] = rriResample[n];
fdData.vlfCount++;
} else if ((currentFrequency >= 0.04F) && (currentFrequency < 0.15F)) {
fdData.lfPsd += (fdData.freqStep * rriResample[n]);
fdData.lfCount++;
} else if (currentFrequency >= 0.15F && currentFrequency <= 0.4F) {
fdData.hfPsd += fdData.freqStep * rriResample[n];
fdData.hfSamples[fdData.hfCount] = rriResample[n];
fdData.hfCount++;
}
if (currentFrequency >= 0.15F) {
if (fdData.peakPower < rriResample[n]) {
fdData.peakPower = rriResample[n];
fdData.peakFrequency = currentFrequency;
}
} else {
fdData.peakPower = 0;
fdData.peakFrequency = 0;
}
}
}
if ((fdData.hfPsd == 0.0) || (fdData.totalPsd == 0.0) || (fdData.lfHfVlfTotalPsd == fdData.vlfPsd) || (fdData.lfHfVlfTotalPsd == 0.0)) {
LOG.error("Invalid PSD values");
return false;
}
return true;
}
private int processDataTrends(float[] data, int dataSize, int[] outputIndices, int maxOutputSize) {
final int trendCount = dataSize - 1;
int[] trends = new int[trendCount];
// Detect initial trends
for (int i = 0; i < trendCount; i++) {
trends[i] = Float.compare(data[i], data[i + 1]); //(data[i] > data[i + 1]) ? 1 : (data[i] < data[i + 1]) ? -1 : 0;
}
// Process trend transitions
int processedIndicesCount = 0;
for (int i = 0; i < (dataSize - 2) && processedIndicesCount < maxOutputSize; i++) {
final int currentTrend = trends[i];
final int nextTrend = trends[i + 1];
if (nextTrend > currentTrend) {
trends[i] = 2;
} else if (nextTrend != currentTrend) {
trends[i] = 0;
} else {
trends[i] = 1;
}
if (trends[i] != 0) {
outputIndices[processedIndicesCount++] = i + 1;
}
}
return processedIndicesCount;
}
private int findDominantPeak(float[] samples, byte length) {
if ((samples == null || length == 0)) {
return -1;
}
float maxValue = 0.0F;
int maxValueIndex = 0;
for (int i = 0; i < length; i++) {
if (samples[i] > maxValue) {
maxValue = samples[i];
maxValueIndex = (byte) i;
}
}
if (length < 3) {
return maxValueIndex;
}
int[] processedIndices = new int[40];
int validIndicesCount = processDataTrends(samples, length, processedIndices, processedIndices.length);
if (validIndicesCount < 0) {
LOG.error("findDominantPeak processDataTrends error");
return -1;
}
if (validIndicesCount == 0) {
return maxValueIndex;
}
float peakValue = 0.0F;
int peakIndex = 0;
for (int i = 0; i < validIndicesCount; i++) {
final int index = processedIndices[i];
if (index >= length) {
LOG.error( "Invalid index: {}", index);
return -1;
}
if (samples[index] > peakValue) {
peakIndex = index;
peakValue = samples[index];
}
}
return peakIndex;
}
private FrequencyDomainData calculateFrequencyDomainData(float[] rriResample, int length) {
FrequencyDomainData result = new FrequencyDomainData();
if (!calculateFFT(rriResample, length)) {
LOG.error("calculateFFT error");
return null;
}
if (!calculateFrequencyDomain(rriResample, length, result)) {
LOG.error( "calculateFrequencyDomainParameters error");
return null;
}
if (result.freqStep == 0.0) {
LOG.error("Invalid frequency step");
return null;
}
int peakEnd = (int) ((result.peakFrequency * 1.2) / result.freqStep);
if ((peakEnd >= length) | (result.totalPsd == 0.0)) {
LOG.error( "Invalid peak frequency");
return null;
}
int vlfPeakIndex = findDominantPeak(result.vlfSamples, result.vlfCount);
int hfPeakIndex = findDominantPeak(result.hfSamples, result.hfCount);
if (hfPeakIndex < 0 || vlfPeakIndex < 0) {
LOG.error("Peak detection failed (VLF: {}, HF: {})", vlfPeakIndex, hfPeakIndex);
return null;
}
result.vlfPeakIndex = vlfPeakIndex;
result.hfPeakIndex = hfPeakIndex;
return result;
}
private FrequencyDomainData calculateFrequencyDomain(int[] rriData, int[] rriCumulativeSum, int firstNotOutlierIndex, byte[] outliers) {
final float[] rriResample = new float[130];
int resampleSize = prepareFrequencyDomainData(firstNotOutlierIndex, rriData, rriCumulativeSum, rriResample, outliers);
if (resampleSize == 0) {
LOG.error("prepareFrequencyDomainData error");
return null;
}
return calculateFrequencyDomainData(rriResample, resampleSize);
}
private float[] calculateHRV(int[] rriData, int[] rriCumulativeSum) {
byte[] outliers = new byte[rriData.length];
int firstNotOutlierIndex = calculateAndRemoveOutliers(rriData, outliers);
if (firstNotOutlierIndex < 0 || firstNotOutlierIndex > rriData.length) {
LOG.error( "Error calculate outliers");
return null;
}
TimeDomainData timeDomainData = calculateTimeDomain(rriData, firstNotOutlierIndex, outliers);
if (timeDomainData == null) {
LOG.error( "Time Domain error");
return null;
}
FrequencyDomainData frequencyDomainData = calculateFrequencyDomain(rriData, rriCumulativeSum, firstNotOutlierIndex, outliers);
if (frequencyDomainData == null) {
LOG.error( "Frequency Domain error");
return null;
}
float pVlf = (float) ((frequencyDomainData.vlfPsd / frequencyDomainData.lfHfVlfTotalPsd) * 100.0);
float pLf = (float) ((frequencyDomainData.lfPsd / frequencyDomainData.lfHfVlfTotalPsd) * 100.0);
boolean isMedianNNIInRange = timeDomainData.medianNNICount >= 0 && timeDomainData.medianNNICount <= 600;
boolean isSDHRInRange = timeDomainData.sdHR >= 1.0 && timeDomainData.sdHR <= 20.0;
boolean isRangeNNILessInRange = timeDomainData.rangeNNI >= 0 && timeDomainData.rangeNNI <= 1000;
boolean isRMSDInRange = timeDomainData.rmsSD >= 0.0 && timeDomainData.rmsSD <= 200.0;
boolean isNormalizedVarianceInRange = timeDomainData.normVar >= 0.0 && timeDomainData.normVar <= 2.0;
boolean isPVlfInRange = pVlf >= 0.0 && pVlf <= 100.0;
boolean isNNIDifferenceInRange = timeDomainData.medianNNI >= 300 && timeDomainData.medianNNI <= 1500;
if (isMedianNNIInRange && isSDHRInRange && isRangeNNILessInRange
&& isRMSDInRange && isNNIDifferenceInRange && isNormalizedVarianceInRange && isPVlfInRange) {
float[] result = new float[10];
result[0] = timeDomainData.medianNNICount;
result[1] = timeDomainData.sdHR;
result[2] = timeDomainData.rangeNNI;
result[3] = pVlf;
result[4] = timeDomainData.rmsSD;
result[5] = timeDomainData.medianNNI;
result[6] = timeDomainData.normVar;
result[7] = frequencyDomainData.hfPsd;
result[8] = frequencyDomainData.lfPsd;
result[9] = pLf;
return result;
}
LOG.error( "validation fail");
return null;
}
public float[] calculateStressHRVParameters(List<Integer> deviceRriData, List<Integer> deviceSqiData, int signalTime) {
if ((signalTime <= 50) || (signalTime >= 70)) {
return null;
}
if ((deviceRriData.size() < 30) || (deviceRriData.size() > 210) || (deviceRriData.size() > deviceSqiData.size())) {
return null;
}
List<Integer> tmpRri = new ArrayList<>();
List<Integer> tmpRriCumulativeSum = new ArrayList<>();
int sum = 0;
for (int i = 0; i < deviceRriData.size(); i++) {
sum = sum + deviceRriData.get(i);
if (deviceSqiData.get(i) == 100) {
tmpRri.add(deviceRriData.get(i));
tmpRriCumulativeSum.add(sum);
}
}
if (tmpRri.isEmpty() || tmpRri.size() < 30) {
LOG.error("No valid RRI signals");
return null;
}
int total = 0;
int validCount = 0;
int[] filteredRri = new int[tmpRri.size()];
int[] rriCumulativeSum = new int[tmpRriCumulativeSum.size()];
for (int i = 0; i < tmpRri.size(); i++) {
filteredRri[i] = tmpRri.get(i);
rriCumulativeSum[i] = tmpRriCumulativeSum.get(i);
if ((filteredRri[i] >= 400) && (filteredRri[i] <= 1400)) {
total += filteredRri[i];
validCount += 1;
}
}
if (rriCumulativeSum[rriCumulativeSum.length - 1] > 65000) {
LOG.error( "rriCumulativeSum is long");
return null;
}
if ((validCount == 0) || (total == 0)) {
LOG.error("validCount or total is 0");
return null;
}
float sqi = (60.0F / (((float) total / validCount) / 1000.0F)) * ((float) signalTime / 60.0F);
if (sqi < 0.2) {
LOG.error("sqi is low");
return null;
}
return calculateHRV(filteredRri, rriCumulativeSum);
}
}
@@ -0,0 +1,64 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress;
public class HuaweiStressScoreCalculation {
private static final float[][] NORMALIZATION_PARAMS = {
{19.381044F, 9.474176F},
{5.5441837F, 1.9308523F},
{184.17795F, 54.48858F},
{33.487267F, 25.893078F},
{32.97501F, 16.267557F},
{677.911F, 130.61485F},
{0.43178082F, 0.1747625F},
{147.99915F, 170.24158F},
{208.32425F, 245.64342F},
{37.63596F, 19.478745F}
};
private static final float SCORE_INIT_VALUE = 3.835272F;
private static final float[] COEFFICIENTS = {0.16605523F, 0.24399279F, 0.0F, 0.0F, -0.07095941F, -0.20609115F, 0.0F, -0.14579488F, -0.09786916F, 0.0F};
private static final float MIN_SCORE = 0.0f;
private static final float MAX_SCORE = 7.0f;
private static final int OUTPUT_MIN = 15;
private static final int OUTPUT_MAX = 90;
public static float calculateScoreFactor(float[] features) {
float[] copyFeatures = features.clone();
for (int i = 0; i < NORMALIZATION_PARAMS.length; i++) {
float mean = NORMALIZATION_PARAMS[i][0];
float stdDev = NORMALIZATION_PARAMS[i][1];
copyFeatures[i] = (copyFeatures[i] - mean) / stdDev;
}
float score = SCORE_INIT_VALUE;
for (int i = 0; i < COEFFICIENTS.length; i++) {
score += copyFeatures[i] * COEFFICIENTS[i];
}
return Math.max(MIN_SCORE, Math.min(score, MAX_SCORE));
}
private static int calculateNormalizedScore(float value) {
final int score = (int) ((value * 14.0f) + 1.5f);
return Math.min(Math.max(score, 1), 99);
}
public static byte calculateNormalizedFinalScore(float scoreFactor) {
final float clampedValue = Math.max(MIN_SCORE, Math.min(scoreFactor, MAX_SCORE));
final int clampedScore = calculateNormalizedScore(clampedValue);
return (byte) Math.min(Math.max(clampedScore, OUTPUT_MIN), OUTPUT_MAX);
}
public static float calibrateScoreFactor(float calibrationScore, float scoreFactor) {
calibrationScore = Math.max(40, Math.min(calibrationScore, 70));
float calibrationScoreFactor = (float) (((calibrationScore - 1) * 7.0) / 98.0);
float clampedCalibrationScoreFactor = Math.max(MIN_SCORE, Math.min(calibrationScoreFactor, MAX_SCORE));
return (float) ((clampedCalibrationScoreFactor * 0.8) + (scoreFactor * 0.2));
}
}
@@ -0,0 +1,65 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui;
import android.os.Bundle;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class HuaweiStressCalibrationActivity extends AbstractGBActivity {
private GBDevice device;
protected String fragmentTag() {
return HuaweiStressCalibrationFragment.FRAGMENT_TAG;
}
protected Fragment newFragment() {
return HuaweiStressCalibrationFragment.newInstance(device);
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
setContentView(R.layout.activity_device_settings);
if (device == null || !device.isInitialized()) {
GB.toast(getString(R.string.watch_not_connected), Toast.LENGTH_SHORT, GB.INFO);
finish();
}
if (savedInstanceState == null) {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(fragmentTag());
if (fragment == null) {
fragment = newFragment();
}
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_container, fragment, fragmentTag())
.commit();
}
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId();
if (itemId == android.R.id.home) {
// Simulate a back press, so that we don't actually exit the activity when
// in a nested PreferenceScreen
this.getOnBackPressedDispatcher().onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
}
@@ -0,0 +1,238 @@
package nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.google.android.material.slider.Slider;
import com.google.android.material.snackbar.Snackbar;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress.HuaweiStressScoreCalculation;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
public class HuaweiStressCalibrationFragment extends AbstractGBFragment {
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressCalibrationFragment.class);
public static final String EXTRA_STRESS_PROGRESS = "huawei_stress_progress";
public static final String EXTRA_STRESS_DATA = "huawei_stress_data";
public static final String EXTRA_STRESS_ERROR = "huawei_stress_error";
static final String FRAGMENT_TAG = "HUAWEI_STRESS_CALIBRATE_FRAGMENT";
public static final String ACTION_STRESS_UPDATE = "nodomain.freeyourgadget.gadgetbridge.huawei.stress.start";
public static final String ACTION_STRESS_RESULT = "nodomain.freeyourgadget.gadgetbridge.huawei.stress.result";
private GBDevice device;
private RelativeLayout layoutMeasure;
private CircularProgressIndicator progress;
private TextView countdownTime;
private Button start;
private RelativeLayout layoutResult;
private TextView score;
private CheckBox isCalibrate;
private Slider calibrateSlider;
private HuaweiStressParser.StressData data = null;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final Bundle arguments = getArguments();
if (arguments == null) {
return null;
}
this.device = arguments.getParcelable(GBDevice.EXTRA_DEVICE);
if (device == null) {
return null;
}
View rootView = inflater.inflate(R.layout.fragment_huawei_stress_calibrate, container, false);
layoutMeasure = rootView.findViewById(R.id.huawei_stress_measure);
progress = rootView.findViewById(R.id.huawei_stress_countdown);
countdownTime = rootView.findViewById(R.id.huawei_stress_countdown_time);
start = rootView.findViewById(R.id.huawei_stress_calibrate_start);
layoutResult = rootView.findViewById(R.id.huawei_stress_result);
score = rootView.findViewById(R.id.huawei_stress_score);
isCalibrate = rootView.findViewById(R.id.huawei_stress_calibrate);
calibrateSlider = rootView.findViewById(R.id.huawei_stress_calibrate_slider);
Button finish = rootView.findViewById(R.id.huawei_stress_calibrate_finish);
Button again = rootView.findViewById(R.id.huawei_stress_calibrate_again);
calibrateSlider.setEnabled(false);
start.setOnClickListener(view -> {
start.setVisibility(View.GONE);
startCalibration();
});
again.setOnClickListener(view -> {
layoutResult.setVisibility(View.GONE);
layoutMeasure.setVisibility(View.VISIBLE);
start.setVisibility(View.VISIBLE);
data = null;
setProgress(60000);
});
finish.setOnClickListener(view -> finishCalibrate());
isCalibrate.setOnClickListener(view -> calculateScore());
calibrateSlider.addOnChangeListener((slider, value, fromUser) -> calculateScore());
final IntentFilter filter = new IntentFilter();
filter.addAction(ACTION_STRESS_UPDATE);
filter.addAction(ACTION_STRESS_RESULT);
LocalBroadcastManager.getInstance(requireContext()).registerReceiver(mReceiver, filter);
return rootView;
}
private void setProgress(long j) {
progress.setProgress((int) (j/1000));
countdownTime.setText(formatTime(j));
}
private void calculateScore() {
if(data == null) {
return;
}
LOG.info("User score: {}", data.scoreFactor);
if(isCalibrate.isChecked()) {
calibrateSlider.setEnabled(true);
float userScore = calibrateSlider.getValue();
LOG.info("User score: {}", userScore);
float adjustedScoreFactor = HuaweiStressScoreCalculation.calibrateScoreFactor(userScore, data.scoreFactor);
data.score = HuaweiStressScoreCalculation.calculateNormalizedFinalScore(adjustedScoreFactor);
} else {
calibrateSlider.setEnabled(false);
data.score = HuaweiStressScoreCalculation.calculateNormalizedFinalScore(data.scoreFactor);
}
score.setText(String.format(Locale.ROOT, "%02d", data.score));
}
public HuaweiStressParser.StressData getStressData(final Intent intent) {
String str = intent.getStringExtra(EXTRA_STRESS_DATA);
if(TextUtils.isEmpty(str)) {
return null;
}
return HuaweiStressParser.stressDataFromJsonStr(str);
}
public void finishCalibrate() {
//Maybe end time should be updated. But I am not sure.
//stressData.endTime = endTime;
String str = HuaweiStressParser.stressDataToJsonStr(data);
if(TextUtils.isEmpty(str)) {
Snackbar.make(layoutMeasure, getString(R.string.huawei_stress_calibrate_error), Snackbar.LENGTH_LONG).show();
start.setVisibility(View.VISIBLE);
return;
}
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(HuaweiConstants.PREF_HUAWEI_STRESS_LAST_DATA, str);
editor.putBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, true);
editor.apply();
GBApplication.deviceService(device).onSendConfiguration(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH);
}
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(final Context context, final Intent intent) {
final String action = intent.getAction();
if (action == null) {
LOG.error("Got null action");
return;
}
switch (action) {
case ACTION_STRESS_UPDATE:
long currentCountDownTime = intent.getLongExtra(EXTRA_STRESS_PROGRESS, 0);
setProgress(currentCountDownTime);
break;
case ACTION_STRESS_RESULT:
boolean isError = intent.getBooleanExtra(EXTRA_STRESS_ERROR, true);
if(isError) {
setProgress(60000);
start.setVisibility(View.VISIBLE);
Snackbar.make(layoutMeasure, context.getString(R.string.huawei_stress_calibrate_error), Snackbar.LENGTH_LONG).show();
} else {
data = getStressData(intent);
layoutMeasure.setVisibility(View.GONE);
layoutResult.setVisibility(View.VISIBLE);
calculateScore();
}
break;
default:
LOG.error("Unknown action {}", action);
break;
}
}
};
private String formatTime(long milliSeconds) {
return String.format(Locale.ROOT, "%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(milliSeconds) - TimeUnit.HOURS.toMinutes(TimeUnit.MILLISECONDS.toHours(milliSeconds)),
TimeUnit.MILLISECONDS.toSeconds(milliSeconds) - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(milliSeconds)));
}
private void setDevice(final GBDevice device) {
final Bundle args = getArguments() != null ? getArguments() : new Bundle();
args.putParcelable("device", device);
setArguments(args);
}
@Override
public void onDestroyView() {
LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(mReceiver);
super.onDestroyView();
}
static HuaweiStressCalibrationFragment newInstance(final GBDevice device) {
final HuaweiStressCalibrationFragment fragment = new HuaweiStressCalibrationFragment();
fragment.setDevice(device);
return fragment;
}
@Nullable
@Override
protected CharSequence getTitle() {
return getString(R.string.huawei_stress_calibrate);
}
private void startCalibration() {
GBApplication.deviceService(device).onSendConfiguration(HuaweiConstants.PREF_HUAWEI_STRESS_CALIBRATE);
}
}
@@ -116,6 +116,7 @@ public class HuaweiFileDownloadManager {
DEBUG,
SLEEP_STATE,
SLEEP_DATA,
RRI,
GPS,
UNKNOWN // Never for input!
}
@@ -173,6 +174,10 @@ public class HuaweiFileDownloadManager {
return new FileRequest("sleep_data.bin", FileType.SLEEP_DATA, supportsTruSleepNewSync, startTime, endTime, fileDownloadCallback);
}
public static FileRequest rriFileRequest(boolean supportsRriNewSync, int startTime, int endTime, FileDownloadCallback fileDownloadCallback) {
return new FileRequest("rrisqi_data.bin", FileType.RRI, supportsRriNewSync, startTime, endTime, fileDownloadCallback);
}
private FileRequest(String filename, FileType fileType, boolean newSync, FileDownloadCallback fileDownloadCallback) {
this.filename = filename;
this.fileType = fileType;
@@ -595,10 +600,23 @@ public class HuaweiFileDownloadManager {
// Old sync only, can never be multiple at the same time
// Assuming currentRequest is the correct one the entire time
// Which may no longer be the case when we implement multi-download for new sync
GetFileParametersRequest getFileParametersRequest = new GetFileParametersRequest(supportProvider,
currentFileRequest.fileType == FileType.SLEEP_STATE ||
currentFileRequest.fileType == FileType.SLEEP_DATA
);
byte fileType;
if (currentFileRequest.fileType == HuaweiFileDownloadManager.FileType.DEBUG)
fileType = 0x00;
else if(currentFileRequest.fileType == FileType.SLEEP_STATE || currentFileRequest.fileType == FileType.SLEEP_DATA)
fileType = 0x01;
else if (currentFileRequest.fileType == HuaweiFileDownloadManager.FileType.GPS)
fileType = 0x02;
else if(currentFileRequest.fileType == HuaweiFileDownloadManager.FileType.RRI)
fileType = 0x04;
else {
currentFileRequest.fileDownloadCallback.downloadException(new HuaweiFileDownloadException(currentFileRequest, "Unknown download type"));
reset();
return;
}
GetFileParametersRequest getFileParametersRequest = new GetFileParametersRequest(supportProvider, fileType);
getFileParametersRequest.setFinalizeReq(new Request.RequestCallback() {
@Override
public void call() {
@@ -0,0 +1,204 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import android.os.CountDownTimer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.HrRriTest;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress.HuaweiStressHRVCalculation;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.stress.HuaweiStressScoreCalculation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendHROpenCloseRequest;
public class HuaweiStressCalibration {
public interface HuaweiStressCalibrateCallback {
void onFinish(HuaweiStressParser.StressData stressData);
void onProgress(long j);
void onError();
}
private static final Logger LOG = LoggerFactory.getLogger(HuaweiStressCalibration.class);
private final HuaweiSupportProvider supportProvider;
private RriDataReceiver rriDataReceiver;
private HuaweiStressCalibrateCallback callback = null;
private List<Integer> rriData = new ArrayList<>();
private List<Integer> sqiData = new ArrayList<>();
private long startTime = 0;
private long endTime = 0;
private CountDownTimer measureTimer = null;
private static class RriDataReceiver extends Request {
List<HrRriTest.RriData.Response.rriSqiData> data;
public RriDataReceiver(HuaweiSupportProvider supportProvider) {
super(supportProvider);
}
@Override
public boolean handleResponse(HuaweiPacket response) {
if ((response.serviceId == HrRriTest.id && response.commandId == HrRriTest.RriData.id)) {
receivedPacket = response;
return true;
}
return false;
}
@Override
public boolean autoRemoveFromResponseHandler() {
// This needs to be removed manually
return false;
}
@Override
protected void processResponse() throws ResponseParseException {
if (this.receivedPacket instanceof HrRriTest.RriData.Response) {
HrRriTest.RriData.Response response = (HrRriTest.RriData.Response) this.receivedPacket;
data = response.containers;
} else {
throw new ResponseTypeMismatchException(this.receivedPacket, HrRriTest.RriData.class);
}
}
}
public HuaweiStressCalibration(HuaweiSupportProvider supportProvider) {
this.supportProvider = supportProvider;
}
private void openOrClose(byte type) {
try {
SendHROpenCloseRequest req = new SendHROpenCloseRequest(this.supportProvider, type);
req.doPerform();
} catch (IOException e) {
LOG.error("Failed to SendHROpenCloseRequest", e);
}
}
public boolean startMeasurements(HuaweiStressCalibrateCallback callback) {
if (this.measureTimer != null) {
// measurement in progress
return false;
}
this.callback = callback;
if (rriDataReceiver == null) {
// We can only init fileDataReceiver if the device is already connected
rriDataReceiver = new RriDataReceiver(supportProvider);
rriDataReceiver.setFinalizeReq(new Request.RequestCallback() {
@Override
public void call() {
for (int i = 0; i < rriDataReceiver.data.size(); i++) {
HrRriTest.RriData.Response.rriSqiData d = rriDataReceiver.data.get(i);
if (d.sqi != 0) {
rriData.add((int) d.rri);
sqiData.add((int) d.sqi);
}
}
}
@Override
public void handleException(Request.ResponseParseException e) {
stopMeasurements(true);
}
});
}
this.supportProvider.addInProgressRequest(rriDataReceiver);
rriData.clear();
sqiData.clear();
startTime = System.currentTimeMillis();
endTime = 0;
openOrClose((byte) 3);
this.measureTimer = new CountDownTimer(60000, 1000) {
public void onTick(long j) {
if (callback != null) {
callback.onProgress(j);
}
}
public void onFinish() {
stopMeasurements(false);
}
};
this.measureTimer.start();
return true;
}
void stopMeasurements(boolean error) {
openOrClose((byte) 4);
endTime = System.currentTimeMillis();
CountDownTimer timer = this.measureTimer;
if (timer != null) {
timer.cancel();
this.measureTimer = null;
}
this.supportProvider.removeInProgressRequests(rriDataReceiver);
if (error) {
if (callback != null) {
callback.onError();
}
return;
}
LOG.info("startTime = {}", startTime);
LOG.info("endTime = {}", endTime);
LOG.info("len = {}", rriData.size());
LOG.info("rri data = {}", Arrays.toString(rriData.toArray()));
LOG.info("sqi data = {}", Arrays.toString(sqiData.toArray()));
int signalTime = (int) (endTime - startTime) / 1000;
HuaweiStressHRVCalculation hrvCalc = new HuaweiStressHRVCalculation();
float[] hvrParams = hrvCalc.calculateStressHRVParameters(rriData, sqiData, signalTime);
if (hvrParams == null || hvrParams.length != 10) {
if (callback != null) {
callback.onError();
}
return;
}
LOG.info("hvrParams = {}", Arrays.toString(hvrParams));
float scoreFactor = HuaweiStressScoreCalculation.calculateScoreFactor(hvrParams);
byte stressScore = HuaweiStressScoreCalculation.calculateNormalizedFinalScore(scoreFactor);
LOG.info("Stress Score = {}", stressScore);
HuaweiStressParser.StressData stressData = new HuaweiStressParser.StressData();
for (float p : hvrParams) {
stressData.features.add(p);
}
// 10 and 11 elements are always 0
stressData.features.add(0.0F);
stressData.features.add(0.0F);
stressData.startTime = startTime;
stressData.endTime = endTime;
stressData.score = stressScore;
stressData.scoreFactor = scoreFactor;
if (this.callback != null) {
callback.onFinish(stressData);
}
}
}
@@ -16,20 +16,21 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_HIGH_ALERT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants.PREF_HUAWEI_HEART_RATE_LOW_ALERT;
import static nodomain.freeyourgadget.gadgetbridge.model.ActivityUser.PREF_USER_GOAL_STANDING_TIME_HOURS;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.location.Location;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -65,11 +66,13 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiDictTypes;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiGpsParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTruSleepParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.CameraRemote;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.GpsAndTime;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui.HuaweiStressCalibrationFragment;
import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummaryDao;
@@ -154,6 +157,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetH
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetMediumToStrengthThresholdRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetSkinTemperatureMeasurement;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetSpO2LowAlert;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetStressRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SetTemperatureUnitSetting;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.StopFindPhoneRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.StopNotificationRequest;
@@ -274,6 +278,7 @@ public class HuaweiSupportProvider {
protected HuaweiOTAManager huaweiOTAManager = new HuaweiOTAManager(this);
HuaweiStressCalibration stressCalibration = null;
public HuaweiCoordinatorSupplier getCoordinator() {
return ((HuaweiCoordinatorSupplier) this.gbDevice.getDeviceCoordinator());
@@ -1139,6 +1144,12 @@ public class HuaweiSupportProvider {
case HuaweiConstants.PREF_HUAWEI_SPO_LOW_ALERT:
setSpoLowAlert();
break;
case HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH:
setStress();
break;
case HuaweiConstants.PREF_HUAWEI_STRESS_CALIBRATE:
calibrateStress();
break;
case DeviceSettingsPreferenceConst.PREF_FORCE_ENABLE_SMART_ALARM:
getAlarms();
break;
@@ -1344,7 +1355,7 @@ public class HuaweiSupportProvider {
getFitnessTotalsRequest.setFinalizeReq(new RequestCallback() {
@Override
public void call() {
if (!downloadTruSleepData(start, end))
if (!(downloadTruSleepData(start, end) && downloadStressData(start, end)))
syncState.setActivitySync(false);
}
@@ -2275,6 +2286,98 @@ public class HuaweiSupportProvider {
}
}
private void setStress() {
boolean automaticStressEnabled = GBApplication
.getDeviceSpecificSharedPrefs(getDevice().getAddress())
.getBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, false);
if(automaticStressEnabled && getLastStressData() == null) {
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress());
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putBoolean(HuaweiConstants.PREF_HUAWEI_STRESS_SWITCH, false);
editor.apply();
GB.toast(context, context.getString(R.string.huawei_stress_no_calibration_data), Toast.LENGTH_SHORT, GB.ERROR);
return;
}
try {
SetStressRequest req = new SetStressRequest(this, automaticStressEnabled);
req.doPerform();
} catch (IOException e) {
GB.toast(context, "Failed to set stress", Toast.LENGTH_SHORT, GB.ERROR, e);
LOG.error("Failed to set stress", e);
}
}
private void calibrateStress() {
if(stressCalibration != null) {
GB.toast(context.getString(R.string.huawei_stress_calibrate_in_progress), Toast.LENGTH_SHORT, GB.INFO);
return;
}
stressCalibration = new HuaweiStressCalibration(this);
boolean ret = stressCalibration.startMeasurements(new HuaweiStressCalibration.HuaweiStressCalibrateCallback() {
@Override
public void onFinish(HuaweiStressParser.StressData stressData) {
stressCalibration = null;
GB.toast(context.getString(R.string.huawei_stress_calibrate_done), Toast.LENGTH_SHORT, GB.INFO);
final Intent intent = new Intent(HuaweiStressCalibrationFragment.ACTION_STRESS_RESULT);
String str = HuaweiStressParser.stressDataToJsonStr(stressData);
if(!TextUtils.isEmpty(str)) {
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_ERROR, false);
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_DATA, str);
} else {
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_ERROR, true);
}
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
@Override
public void onProgress(long j) {
final Intent intent = new Intent(HuaweiStressCalibrationFragment.ACTION_STRESS_UPDATE);
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_PROGRESS, j);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
}
@Override
public void onError() {
stressCalibration = null;
final Intent intent = new Intent(HuaweiStressCalibrationFragment.ACTION_STRESS_RESULT);
intent.putExtra(HuaweiStressCalibrationFragment.EXTRA_STRESS_ERROR, true);
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
GB.toast(context.getString(R.string.huawei_stress_calibrate_error), Toast.LENGTH_SHORT, GB.ERROR);
}
});
if(ret) {
GB.toast(context.getString(R.string.huawei_stress_calibrate_started), Toast.LENGTH_SHORT, GB.INFO);
} else {
GB.toast(context.getString(R.string.huawei_stress_calibrate_in_progress), Toast.LENGTH_SHORT, GB.INFO);
}
}
public void storeLastStressData(HuaweiStressParser.StressData data) {
String str = HuaweiStressParser.stressDataToJsonStr(data);
if(TextUtils.isEmpty(str)) {
LOG.error("Failed to store stress data");
return;
}
SharedPreferences sharedPrefs = GBApplication.getDeviceSpecificSharedPrefs(deviceMac);
SharedPreferences.Editor editor = sharedPrefs.edit();
editor.putString(HuaweiConstants.PREF_HUAWEI_STRESS_LAST_DATA, str);
editor.apply();
}
public HuaweiStressParser.StressData getLastStressData() {
String str = GBApplication
.getDeviceSpecificSharedPrefs(this.getDevice().getAddress())
.getString(HuaweiConstants.PREF_HUAWEI_STRESS_LAST_DATA, "");
if(TextUtils.isEmpty(str)) {
LOG.error("Failed to get saved stress data");
return null;
}
return HuaweiStressParser.stressDataFromJsonStr(str);
}
public void sendDebugRequest() {
try {
LOG.debug("Send debug request");
@@ -2615,6 +2718,47 @@ public class HuaweiSupportProvider {
return true;
}
public boolean downloadStressData(int start, int end) {
if (!getHuaweiCoordinator().supportsAutoStress())
return false;
huaweiFileDownloadManager.addToQueue(HuaweiFileDownloadManager.FileRequest.rriFileRequest(
getHuaweiCoordinator().getSupportsRriNewSync(),
start,
end,
new HuaweiFileDownloadManager.FileDownloadCallback() {
@Override
public void downloadComplete(HuaweiFileDownloadManager.FileRequest fileRequest) {
if (fileRequest.getData().length != 0) {
LOG.debug("Parsing stress file");
HuaweiStressParser.RriFileData results = HuaweiStressParser.parseRri(fileRequest.getData());
LOG.info("stress result: {}", results);
// TODO: process and save
if(results != null && !results.stressData.isEmpty()) {
HuaweiStressParser.StressData stressData = results.stressData.get(results.stressData.size() - 1);
LOG.info("Last stored stress data: {}", stressData);
HuaweiStressParser.StressData currentStressData = getLastStressData();
if(currentStressData == null || stressData.endTime > currentStressData.endTime) {
storeLastStressData(stressData);
}
}
} else {
LOG.debug("Stress file empty");
}
syncState.setActivitySync(false);
}
@Override
public void downloadException(HuaweiFileDownloadManager.HuaweiFileDownloadException e) {
super.downloadException(e);
syncState.setActivitySync(false);
}
}
), true);
return true;
}
public void endOfWorkoutSync() {
this.syncState.setWorkoutSync(false);
}
@@ -2744,6 +2888,7 @@ public class HuaweiSupportProvider {
syncState.updateState(needSync);
}
public void onTestNewFunction() {
// Show to user
gbDevice.setBusyTask("Downloading files...");
@@ -57,6 +57,8 @@ public class GetFileDownloadInitRequest extends Request {
return FileDownloadService2C.FileType.SLEEP_STATE;
case SLEEP_DATA:
return FileDownloadService2C.FileType.SLEEP_DATA;
case RRI:
return FileDownloadService2C.FileType.RRI;
case GPS:
return FileDownloadService2C.FileType.GPS;
default:
@@ -70,6 +72,8 @@ public class GetFileDownloadInitRequest extends Request {
return HuaweiFileDownloadManager.FileType.SLEEP_STATE;
case SLEEP_DATA:
return HuaweiFileDownloadManager.FileType.SLEEP_DATA;
case RRI:
return HuaweiFileDownloadManager.FileType.RRI;
case GPS:
return HuaweiFileDownloadManager.FileType.GPS;
default:
@@ -92,6 +96,8 @@ public class GetFileDownloadInitRequest extends Request {
return new FileDownloadService0A.FileDownloadInit.SleepFilesRequest(paramsProvider, request.getStartTime(), request.getEndTime()).serialize();
else if (this.request.getFileType() == HuaweiFileDownloadManager.FileType.GPS)
return new FileDownloadService0A.FileDownloadInit.GpsFileRequest(paramsProvider, request.getWorkoutId()).serialize();
else if(this.request.getFileType() == HuaweiFileDownloadManager.FileType.RRI)
return new FileDownloadService0A.FileDownloadInit.RriFileRequest(paramsProvider, request.getStartTime(), request.getEndTime()).serialize();
else
throw new RequestCreationException("Unknown file type");
}
@@ -24,22 +24,22 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupport
public class GetFileParametersRequest extends Request {
private final boolean truSleep;
private final byte fileType;
private int maxBlockSize;
private int timeout;
public GetFileParametersRequest(HuaweiSupportProvider support, boolean truSleep) {
public GetFileParametersRequest(HuaweiSupportProvider support, byte fileType) {
super(support);
this.serviceId = FileDownloadService0A.id;
this.commandId = FileDownloadService0A.FileParameters.id;
this.truSleep = truSleep;
this.fileType = fileType;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
try {
return new FileDownloadService0A.FileParameters.Request(paramsProvider, truSleep).serialize();
return new FileDownloadService0A.FileParameters.Request(paramsProvider, fileType).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
@@ -56,6 +56,7 @@ public class GetSettingRelatedRequest extends Request {
LOG.debug("handle Setting Related");
supportProvider.getHuaweiCoordinator().setSupportsTruSleepNewSync(((DeviceConfig.SettingRelated.Response) receivedPacket).truSleepNewSync);
supportProvider.getHuaweiCoordinator().setSupportsRriNewSync(((DeviceConfig.SettingRelated.Response) receivedPacket).rriNewSync);
supportProvider.getHuaweiCoordinator().setSupportsGpsNewSync(((DeviceConfig.SettingRelated.Response) receivedPacket).gpsNewSync);
}
}
@@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.HrRriTest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SendHROpenCloseRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(SendHROpenCloseRequest.class);
private final byte type;
public SendHROpenCloseRequest(HuaweiSupportProvider support, byte type) {
super(support);
this.serviceId = HrRriTest.id;
this.commandId = HrRriTest.OpenOrClose.id;
this.type = type;
this.addToResponse = false;
}
@Override
protected List<byte[]> createRequest() throws Request.RequestCreationException {
try {
return new HrRriTest.OpenOrClose.Request(paramsProvider, this.type).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new Request.RequestCreationException(e);
}
}
}
@@ -0,0 +1,55 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiStressParser;
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Stress;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
public class SetStressRequest extends Request {
private static final Logger LOG = LoggerFactory.getLogger(SetStressRequest.class);
private final boolean automaticStressEnabled;
public SetStressRequest(HuaweiSupportProvider support, boolean automaticStressEnabled) {
super(support);
this.serviceId = Stress.id;
this.commandId = Stress.AutomaticStress.id;
this.addToResponse = false;
this.automaticStressEnabled = automaticStressEnabled;
}
@Override
protected List<byte[]> createRequest() throws RequestCreationException {
byte status;
List<Float> features = null;
byte score = 0;
long time = 0;
if (automaticStressEnabled) {
status = 1;
HuaweiStressParser.StressData stressData = supportProvider.getLastStressData();
if(stressData == null || stressData.score == 0 || stressData.endTime == 0) {
throw new RequestCreationException("No data for activate");
}
features = stressData.features;
score = stressData.score;
time = stressData.endTime;
LOG.info("Attempting to enable automatic stress. Stress data {}", stressData);
} else {
LOG.info("Attempting to disable automatic stress");
status = 2;
}
try {
return new Stress.AutomaticStress.Request(paramsProvider, status, score, features, (int) (time / 1000)).serialize();
} catch (HuaweiPacket.CryptoException e) {
throw new RequestCreationException(e);
}
}
}
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/huawei_stress_measure"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/huawei_stress_countdown"
android:layout_centerHorizontal="true"
android:layout_marginBottom="30dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:gravity="center"
android:text="@string/huawei_stress_calibrate_information" />
<com.google.android.material.progressindicator.CircularProgressIndicator
android:id="@+id/huawei_stress_countdown"
android:layout_width="270dp"
android:layout_height="270dp"
app:indicatorSize="200dp"
app:indicatorColor="@color/accent"
app:trackThickness="10dp"
android:layout_centerInParent="true"
android:indeterminate="false"
android:max="60"
android:progress="60" />
<TextView
android:id="@+id/huawei_stress_countdown_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="60:00"
android:textSize="40sp" />
<Button
android:id="@+id/huawei_stress_calibrate_start"
android:layout_width="270dp"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_below="@+id/huawei_stress_countdown"
android:layout_centerHorizontal="true"
android:text="@string/start" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/huawei_stress_result"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<Button
android:id="@+id/huawei_stress_calibrate_again"
android:layout_width="270dp"
android:layout_height="wrap_content"
android:layout_marginBottom="30dp"
android:layout_above="@+id/huawei_stress_score"
android:layout_centerHorizontal="true"
android:text="@string/huawei_stress_calibrate_again"
/>
<TextView
android:id="@+id/huawei_stress_score"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="@string/n_a"
android:textSize="80sp" />
<CheckBox
android:id="@+id/huawei_stress_calibrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/huawei_stress_score"
android:checked="false"
android:text="@string/huawei_stress_calibrate_adjust_score" />
<com.google.android.material.slider.Slider
android:id="@+id/huawei_stress_calibrate_slider"
android:layout_below="@+id/huawei_stress_calibrate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:valueFrom="40"
android:valueTo="70"
android:value="55"
android:stepSize="1" />
<Button
android:id="@+id/huawei_stress_calibrate_finish"
android:layout_width="270dp"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:layout_below="@+id/huawei_stress_calibrate_slider"
android:layout_centerHorizontal="true"
android:text="@string/huawei_stress_calibrate_finish"
/>
</RelativeLayout>
</RelativeLayout>
+16
View File
@@ -3751,4 +3751,20 @@
<string name="ultrahuman_airplane_mode_question">Activate airplane mode?</string>
<string name="ultrahuman_airplane_mode_title">airplane mode</string>
<string name="ultrahuman_unhandled_error_response">received unhandled response code 0x%x for operation 0x%x</string>
<string name="huawei_stress_test_enable">Automatic stress test</string>
<string name="huawei_stress_test_enable_summary">Enable or disable automatic stress test</string>
<string name="huawei_stress_no_calibration_data">No initial data - calibrate first</string>
<string name="huawei_stress_calibrate">Calibrate</string>
<string name="huawei_stress_calibrate_summary">Calibrate automatic stress test</string>
<string name="huawei_stress_calibrate_started">Stress calibration has been started</string>
<string name="huawei_stress_calibrate_in_progress">Stress calibration already in progress</string>
<string name="huawei_stress_calibrate_done">Stress calibration is done</string>
<string name="huawei_stress_calibrate_error">Stress calibration error</string>
<string name="huawei_stress_calibrate_information">For optimal results rest for a while before starting a test and keep still during the test. \n Fasten the device tightly, with the screen facing up and the sensor in direct contact with your skin.</string>
<string name="huawei_stress_calibrate_start">Start</string>
<string name="huawei_stress_calibrate_finish">Finish</string>
<string name="huawei_stress_calibrate_again">Calibrate again</string>
<string name="huawei_stress_calibrate_adjust_score">Adjust score</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:icon="@drawable/ic_heart"
android:key="screen_huawei_stress"
android:persistent="false"
android:title="@string/menuitem_stress">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_huawei_stress_switch"
android:layout="@layout/preference_checkbox"
android:title="@string/huawei_stress_test_enable"
android:summary="@string/huawei_stress_test_enable_summary" />
<Preference
android:key="pref_huawei_stress_perform_calibrate"
android:title="@string/huawei_stress_calibrate"
android:summary="@string/huawei_stress_calibrate_summary">
</Preference>
</PreferenceScreen>
</androidx.preference.PreferenceScreen>