mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Huawei: ability to view heart rate zones and limits.
This commit is contained in:
@@ -247,6 +247,10 @@
|
||||
android:name=".devices.huawei.ui.HuaweiStressCalibrationActivity"
|
||||
android:label="@string/huawei_stress_calibrate"
|
||||
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.heartratezones.HeartRateSettingsActivity"
|
||||
android:label="@string/heart_rate_settings"
|
||||
android:parentActivityName=".activities.devicesettings.DeviceSettingsActivity" />
|
||||
<activity
|
||||
android:name=".activities.NotificationsAppIconUploadActivity"
|
||||
android:label="@string/notifications_app_icon_upload"
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.heartratezones;
|
||||
|
||||
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 HeartRateSettingsActivity extends AbstractGBActivity {
|
||||
private GBDevice device;
|
||||
|
||||
|
||||
protected String fragmentTag() {
|
||||
return HeartRateSettingsFragment.FRAGMENT_TAG;
|
||||
}
|
||||
|
||||
|
||||
protected Fragment newFragment() {
|
||||
return HeartRateSettingsFragment.newInstance(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
device = getIntent().getParcelableExtra(GBDevice.EXTRA_DEVICE);
|
||||
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
if (device == null) {
|
||||
GB.toast(getString(R.string.device_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);
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.heartratezones;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.MenuProvider;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBFragment;
|
||||
import nodomain.freeyourgadget.gadgetbridge.databinding.FragmentHeartRateSettingsBinding;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceMusicPlaylist;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class HeartRateSettingsFragment extends AbstractGBFragment implements MenuProvider {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HeartRateSettingsFragment.class);
|
||||
|
||||
static final String FRAGMENT_TAG = "HUAWEI_HEART_RATE_SETTINGS_FRAGMENT";
|
||||
|
||||
|
||||
private FragmentHeartRateSettingsBinding binding;
|
||||
|
||||
private GBDevice device;
|
||||
|
||||
private HeartRateZonesSpec spec;
|
||||
|
||||
private List<HeartRateZonesConfig> config;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
|
||||
final Bundle arguments = getArguments();
|
||||
if (arguments == null) {
|
||||
return null;
|
||||
}
|
||||
device = arguments.getParcelable(GBDevice.EXTRA_DEVICE);
|
||||
if (device == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final DeviceCoordinator coordinator = device.getDeviceCoordinator();
|
||||
|
||||
binding = FragmentHeartRateSettingsBinding.inflate(inflater);
|
||||
View view = binding.getRoot();
|
||||
|
||||
spec = coordinator.getHeartRateZonesSpec(device);
|
||||
|
||||
config = spec.getDeviceConfig();
|
||||
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (HeartRateZonesConfig entry : config) {
|
||||
binding.hrSettingsTabLayout.addTab(binding.hrSettingsTabLayout.newTab().setText(spec.getNameByType(requireContext(), entry.getType())));
|
||||
}
|
||||
|
||||
if (config.size() == 1) {
|
||||
binding.hrSettingsTabLayout.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.hrSettingsTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {
|
||||
updateData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
binding.hrSettingsZones.calculationMethodSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
|
||||
updateZones();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNothingSelected(AdapterView<?> adapterView) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
binding.hrSettingsLimits.highHrWarning.setOnCheckedChangeListener((compoundButton, b) -> saveCurrentLimitWarning(b));
|
||||
binding.saveHrZonesSettings.setOnClickListener(view2 -> saveCurrentConfig());
|
||||
|
||||
updateData();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
requireActivity().addMenuProvider(this, getViewLifecycleOwner(), Lifecycle.State.RESUMED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_heart_rate_zones, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.hr_zones_reset_all) {
|
||||
if (spec != null) {
|
||||
new MaterialAlertDialogBuilder(this.requireContext())
|
||||
.setTitle(R.string.hr_settings_zones_remove_all)
|
||||
.setMessage(this.getString(R.string.hr_settings_zones_remove_all_description))
|
||||
.setIcon(R.drawable.ic_warning)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
spec.clearConfig();
|
||||
config = spec.getDeviceConfig();
|
||||
updateData();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private static class CalculateMethodItem {
|
||||
private final String name;
|
||||
private final HeartRateZones.CalculationMethod method;
|
||||
|
||||
public CalculateMethodItem(@NonNull String name, HeartRateZones.CalculationMethod method) {
|
||||
this.name = name;
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public HeartRateZones.CalculationMethod getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
private HeartRateZonesConfig getSelectedHRConfig() {
|
||||
int pos = binding.hrSettingsTabLayout.getSelectedTabPosition();
|
||||
return config.get(pos);
|
||||
}
|
||||
|
||||
private void saveCurrentConfig() {
|
||||
final HeartRateZonesConfig hrConfig = getSelectedHRConfig();
|
||||
spec.saveConfig(hrConfig);
|
||||
}
|
||||
|
||||
private void saveCurrentLimitWarning(boolean checked) {
|
||||
HeartRateZonesConfig hrConfig = getSelectedHRConfig();
|
||||
hrConfig.setWarningEnable(checked);
|
||||
}
|
||||
|
||||
private void updateData() {
|
||||
final HeartRateZonesConfig hrConfig = getSelectedHRConfig();
|
||||
if (hrConfig == null)
|
||||
return;
|
||||
|
||||
List<CalculateMethodItem> types = new ArrayList<>();
|
||||
|
||||
List<HeartRateZones> methods = hrConfig.getConfigByMethods();
|
||||
for (HeartRateZones zn : methods) {
|
||||
types.add(new CalculateMethodItem(HeartRateZones.methodToString(requireContext(), zn.getMethod()), zn.getMethod()));
|
||||
}
|
||||
|
||||
CalculateMethodItem[] myStringArray = types.toArray(new CalculateMethodItem[0]);
|
||||
ArrayAdapter<CalculateMethodItem> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, myStringArray);
|
||||
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
|
||||
binding.hrSettingsZones.calculationMethodSpinner.setAdapter(adapter);
|
||||
|
||||
int idx = 0;
|
||||
for (int i = 0; i < myStringArray.length; i++) {
|
||||
if (myStringArray[i].method == hrConfig.getCurrentCalculationMethod()) {
|
||||
idx = i;
|
||||
}
|
||||
}
|
||||
binding.hrSettingsZones.calculationMethodSpinner.setSelection(idx);
|
||||
|
||||
binding.hrSettingsLimits.limitHighHr.setText(String.valueOf(hrConfig.getWarningHRLimit()));
|
||||
binding.hrSettingsLimits.highHrWarning.setChecked(hrConfig.getWarningEnable());
|
||||
|
||||
}
|
||||
|
||||
private void updateZones() {
|
||||
HeartRateZonesConfig hrConfig = getSelectedHRConfig();
|
||||
if (hrConfig == null)
|
||||
return;
|
||||
|
||||
final CalculateMethodItem calcMethod = (CalculateMethodItem) binding.hrSettingsZones.calculationMethodSpinner.getSelectedItem();
|
||||
|
||||
HeartRateZones selected = null;
|
||||
List<HeartRateZones> methods = hrConfig.getConfigByMethods();
|
||||
for (HeartRateZones zn : methods) {
|
||||
if (calcMethod.getMethod() == zn.getMethod()) {
|
||||
selected = zn;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (selected == null)
|
||||
return;
|
||||
|
||||
hrConfig.setCurrentCalculationMethod(calcMethod.getMethod());
|
||||
|
||||
if (calcMethod.getMethod() == HeartRateZones.CalculationMethod.LTHR) {
|
||||
binding.hrSettingsZones.zonesMaxHrTitle.setText(getString(R.string.hr_settings_zones_lactate_threshold_hr));
|
||||
} else {
|
||||
binding.hrSettingsZones.zonesMaxHrTitle.setText(getString(R.string.hr_settings_zones_max_hr));
|
||||
}
|
||||
binding.hrSettingsZones.zonesMaxHr.setText(String.valueOf(selected.getHRThreshold()));
|
||||
|
||||
if (calcMethod.getMethod() == HeartRateZones.CalculationMethod.HRR) {
|
||||
binding.hrSettingsZones.layoutZonesRestingHr.setVisibility(View.VISIBLE);
|
||||
binding.hrSettingsZones.zonesRestingHr.setText(String.valueOf(selected.getHRResting()));
|
||||
} else {
|
||||
binding.hrSettingsZones.layoutZonesRestingHr.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (calcMethod.getMethod() == HeartRateZones.CalculationMethod.LTHR) {
|
||||
binding.hrSettingsZones.heartRateZoneMaxValue.setText(getString(R.string.n_a));
|
||||
binding.hrSettingsZones.heartRateZoneMaxPercent.setText(getString(R.string.n_a));
|
||||
|
||||
} else {
|
||||
binding.hrSettingsZones.heartRateZoneMaxValue.setText(String.valueOf(selected.getHRThreshold()));
|
||||
binding.hrSettingsZones.heartRateZoneMaxPercent.setText(String.valueOf(100));
|
||||
}
|
||||
|
||||
binding.hrSettingsZones.heartRateZone5Value.setText(String.valueOf(selected.getZone5()));
|
||||
binding.hrSettingsZones.heartRateZone4Value.setText(String.valueOf(selected.getZone4()));
|
||||
binding.hrSettingsZones.heartRateZone3Value.setText(String.valueOf(selected.getZone3()));
|
||||
binding.hrSettingsZones.heartRateZone2Value.setText(String.valueOf(selected.getZone2()));
|
||||
binding.hrSettingsZones.heartRateZone1Value.setText(String.valueOf(selected.getZone1()));
|
||||
|
||||
binding.hrSettingsZones.heartRateZone5Percent.setText(String.valueOf(selected.getPercentage(selected.getZone5())));
|
||||
binding.hrSettingsZones.heartRateZone4Percent.setText(String.valueOf(selected.getPercentage(selected.getZone4())));
|
||||
binding.hrSettingsZones.heartRateZone3Percent.setText(String.valueOf(selected.getPercentage(selected.getZone3())));
|
||||
binding.hrSettingsZones.heartRateZone2Percent.setText(String.valueOf(selected.getPercentage(selected.getZone2())));
|
||||
binding.hrSettingsZones.heartRateZone1Percent.setText(String.valueOf(selected.getPercentage(selected.getZone1())));
|
||||
}
|
||||
|
||||
private void setDevice(final GBDevice device) {
|
||||
final Bundle args = getArguments() != null ? getArguments() : new Bundle();
|
||||
args.putParcelable("device", device);
|
||||
setArguments(args);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
static HeartRateSettingsFragment newInstance(final GBDevice device) {
|
||||
final HeartRateSettingsFragment fragment = new HeartRateSettingsFragment();
|
||||
fragment.setDevice(device);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected CharSequence getTitle() {
|
||||
return getString(R.string.heart_rate_settings);
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.activities.heartratezones;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class HeartRateZonesDividerView extends View {
|
||||
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final int linePx;
|
||||
|
||||
public HeartRateZonesDividerView(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public HeartRateZonesDividerView(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public HeartRateZonesDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
this.linePx = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.0f, getResources().getDisplayMetrics());
|
||||
this.paint.setColor(Color.GRAY);
|
||||
this.paint.setStrokeWidth(this.linePx);
|
||||
this.paint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(@NonNull Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
int width = getWidth();
|
||||
int height = getHeight();
|
||||
int horizontalCenter = width / 2;
|
||||
int verticalCenter = height / 2;
|
||||
canvas.save();
|
||||
canvas.drawLine(horizontalCenter, this.linePx, width, this.linePx, this.paint);
|
||||
canvas.drawLine(0.0f, verticalCenter, horizontalCenter, verticalCenter, this.paint);
|
||||
canvas.drawLine(horizontalCenter, this.linePx, horizontalCenter, height - this.linePx, this.paint);
|
||||
canvas.drawLine(horizontalCenter, height - this.linePx, width, height - this.linePx, this.paint);
|
||||
canvas.restore();
|
||||
}
|
||||
}
|
||||
+6
@@ -96,6 +96,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeightSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WorkoutLoadSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.FileUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
@@ -1105,4 +1106,9 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
public List<DeviceCardAction> getCustomActions() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Vo2MaxSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeightSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WorkoutLoadSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.ServiceDeviceSupport;
|
||||
|
||||
@@ -914,4 +915,6 @@ public interface DeviceCoordinator {
|
||||
List<DeviceCardAction> getCustomActions();
|
||||
|
||||
DeviceKind getDeviceKind(@NonNull GBDevice device);
|
||||
|
||||
HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device);
|
||||
}
|
||||
|
||||
-297
@@ -1,297 +0,0 @@
|
||||
/* Copyright (C) 2024 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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;
|
||||
|
||||
public static final int CALCULATE_METHOD_MHR = 0;
|
||||
public static final int CALCULATE_METHOD_HRR = 1;
|
||||
public static final int CALCULATE_METHOD_LTHR = 3;
|
||||
|
||||
|
||||
private static final int DEFAULT_REST_HEART_RATE = 60;
|
||||
public static final int MAXIMUM_HEART_RATE = 220;
|
||||
|
||||
private final int configType;
|
||||
private int calculateMethod = CALCULATE_METHOD_MHR; // 0 - MHR, 1 - HRR, 3 - LTHR
|
||||
|
||||
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() {
|
||||
return switch (this.configType) {
|
||||
case TYPE_SITTING -> 6;
|
||||
case TYPE_SWIMMING -> 10;
|
||||
case TYPE_OTHER -> 5;
|
||||
default -> 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;
|
||||
}
|
||||
|
||||
private int getZoneForHR(int heartRate, int zone5Threshold, int zone4Threshold, int zone3Threshold, int zone2Threshold, int zone1Threshold) {
|
||||
if (heartRate >= MAXIMUM_HEART_RATE) {
|
||||
return -1;
|
||||
}
|
||||
if (heartRate >= zone5Threshold) {
|
||||
return 4;
|
||||
}
|
||||
if (heartRate >= zone4Threshold) {
|
||||
return 3;
|
||||
}
|
||||
if (heartRate >= zone3Threshold) {
|
||||
return 2;
|
||||
}
|
||||
if (heartRate >= zone2Threshold) {
|
||||
return 1;
|
||||
}
|
||||
return heartRate >= zone1Threshold ? 0 : -1;
|
||||
}
|
||||
|
||||
public int getMHRZone(int heartRate) {
|
||||
return getZoneForHR(heartRate, MHRExtreme, MHRAnaerobic, MHRAerobic, MHRFatBurning, MHRWarmUp);
|
||||
}
|
||||
|
||||
public int getHHRZone(int heartRate) {
|
||||
return getZoneForHR(heartRate, HRRAdvancedAnaerobic, HRRBasicAnaerobic, HRRLactate, HRRAdvancedAerobic, HRRBasicAerobic);
|
||||
}
|
||||
|
||||
public int getLTHRZone(int heartRate) {
|
||||
return getZoneForHR(heartRate, LTHRAnaerobic, LTHRLactate, LTHRAdvancedAerobic, LTHRBasicAerobic, LTHRWarmUp);
|
||||
}
|
||||
|
||||
public int getZoneByMethod(int heartRate, int method) {
|
||||
if(method == CALCULATE_METHOD_LTHR) {
|
||||
return getLTHRZone(heartRate);
|
||||
} else if(method == CALCULATE_METHOD_MHR) {
|
||||
return getMHRZone(heartRate);
|
||||
}
|
||||
return getHHRZone(heartRate);
|
||||
}
|
||||
|
||||
public static boolean isCalculateMethodValidForPostureType(int type, int method) {
|
||||
if(method == CALCULATE_METHOD_LTHR && type == TYPE_UPRIGHT) {
|
||||
return true;
|
||||
}
|
||||
return (method == CALCULATE_METHOD_MHR) || (method == CALCULATE_METHOD_HRR);
|
||||
}
|
||||
|
||||
}
|
||||
+6
@@ -44,6 +44,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiBRSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
@@ -315,4 +316,9 @@ public abstract class HuaweiBRCoordinator extends AbstractBLClassicDeviceCoordin
|
||||
public boolean addBatteryPollingSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
|
||||
return huaweiCoordinator.getHeartRateZonesSpec(device);
|
||||
}
|
||||
}
|
||||
|
||||
+7
@@ -60,6 +60,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSummarySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.HuaweiWorkoutSwimSegmentsSampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*;
|
||||
@@ -344,6 +345,9 @@ public class HuaweiCoordinator {
|
||||
if (supportsSendingGps())
|
||||
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.WORKOUT, R.xml.devicesettings_workout_send_gps_to_band);
|
||||
|
||||
if(supportsTrack() || supportsHeartRateZones())
|
||||
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.WORKOUT, R.xml.devicesettings_heartrate_settings);
|
||||
|
||||
// Other
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_find_phone);
|
||||
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_disable_find_phone_with_dnd);
|
||||
@@ -1140,4 +1144,7 @@ public class HuaweiCoordinator {
|
||||
return supportsTruSleep() && supportsDictSleepSync();
|
||||
}
|
||||
|
||||
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
|
||||
return new HuaweiHeartRateZonesSpec(device, this);
|
||||
}
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
|
||||
|
||||
public class HuaweiHeartRateZones extends HeartRateZones {
|
||||
|
||||
public HuaweiHeartRateZones(CalculationMethod method, int HRThreshold) {
|
||||
super(method, HRThreshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
switch(this.method) {
|
||||
case MHR -> calculateMHRZonesConfig();
|
||||
case HRR -> defaultHRRZonesConfig();
|
||||
case LTHR -> defaultLTHRZonesConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getPercentage(int zone) {
|
||||
return switch(this.method) {
|
||||
case MHR, LTHR -> Math.round((float) zone * 100.0 / (float) HRThreshold);
|
||||
case HRR -> Math.round((float) (zone - HRResting) * 100.0 / (float) (HRThreshold - HRResting));
|
||||
};
|
||||
}
|
||||
|
||||
private void calculateMHRZonesConfig() {
|
||||
zone5 = Math.round(((float) (HRThreshold * 90)) / 100.0f); // Extreme;
|
||||
zone4 = Math.round(((float) (HRThreshold * 80)) / 100.0f); // Anaerobic;
|
||||
zone3 = Math.round(((float) (HRThreshold * 70)) / 100.0f); // Aerobic;
|
||||
zone2 = Math.round(((float) (HRThreshold * 60)) / 100.0f); // FatBurning;
|
||||
zone1 = Math.round(((float) (HRThreshold * 50)) / 100.0f); // WarmUp;
|
||||
}
|
||||
|
||||
private void defaultHRRZonesConfig() {
|
||||
int calcHR = HRThreshold - HRResting;
|
||||
zone5 = Math.round(((float) (calcHR * 95)) / 100.0f) + HRResting; // AdvancedAnaerobic
|
||||
zone4 = Math.round(((float) (calcHR * 88)) / 100.0f) + HRResting; // BasicAnaerobic
|
||||
zone3 = Math.round(((float) (calcHR * 84)) / 100.0f) + HRResting; // Lactate
|
||||
zone2 = Math.round(((float) (calcHR * 74)) / 100.0f) + HRResting; // AdvancedAerobic
|
||||
zone1 = Math.round(((float) (calcHR * 59)) / 100.0f) + HRResting; // BasicAerobic
|
||||
}
|
||||
|
||||
private void defaultLTHRZonesConfig() {
|
||||
HRThreshold = Math.round(((float) HRResting) + (((float) ((HRThreshold - HRResting) * 85)) / 100.0f)); // LTHRThresholdHeartRate
|
||||
zone5 = Math.round(((float) (HRThreshold * 102)) / 100.0f); // Anaerobic
|
||||
zone4 = Math.round(((float) (HRThreshold * 97)) / 100.0f); // Lactate
|
||||
zone3 = Math.round(((float) (HRThreshold * 89)) / 100.0f); // AdvancedAerobic
|
||||
zone2 = Math.round(((float) (HRThreshold * 80)) / 100.0f); // BasicAerobic
|
||||
zone1 = Math.round(((float) (HRThreshold * 67)) / 100.0f); // WarmUp
|
||||
}
|
||||
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.huawei;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesUtils;
|
||||
|
||||
public class HuaweiHeartRateZonesSpec extends HeartRateZonesSpec {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiHeartRateZonesSpec.class);
|
||||
|
||||
public static final int HUAWEI_TYPE_UPRIGHT = 1;
|
||||
public static final int HUAWEI_TYPE_SITTING = 2;
|
||||
public static final int HUAWEI_TYPE_SWIMMING = 3;
|
||||
public static final int HUAWEI_TYPE_OTHER = 4;
|
||||
|
||||
public static final int HUAWEI_CALCULATE_METHOD_MHR = 0;
|
||||
public static final int HUAWEI_CALCULATE_METHOD_HRR = 1;
|
||||
public static final int HUAWEI_CALCULATE_METHOD_LTHR = 3;
|
||||
|
||||
private final HuaweiCoordinator coordinator;
|
||||
|
||||
public HuaweiHeartRateZonesSpec(GBDevice device, HuaweiCoordinator coordinator) {
|
||||
super(device);
|
||||
this.coordinator = coordinator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNameByType(Context context, HeartRateZonesSpec.PostureType type) {
|
||||
return switch (type) {
|
||||
case UPRIGHT -> context.getString(R.string.workout_posture_type_upright);
|
||||
case SITTING -> context.getString(R.string.workout_posture_type_sitting);
|
||||
case SWIMMING -> context.getString(R.string.activity_type_swimming);
|
||||
case OTHER -> context.getString(R.string.activity_type_other);
|
||||
};
|
||||
}
|
||||
|
||||
private HeartRateZones loadOrCreateZones(HeartRateZonesSpec.PostureType type, HeartRateZones.CalculationMethod method, int maxHRThreshold) {
|
||||
String prefs = getHrZoneConfig(type, method);
|
||||
if (prefs != null) {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
return gson.fromJson(prefs, HuaweiHeartRateZones.class);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Error load heart zones config from prefs ", e);
|
||||
}
|
||||
}
|
||||
return new HuaweiHeartRateZones(method, maxHRThreshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<HeartRateZonesConfig> getDeviceConfig() {
|
||||
List<HeartRateZonesConfig> res = new ArrayList<>();
|
||||
|
||||
int age = new ActivityUser().getAge();
|
||||
|
||||
if (!coordinator.supportsTrack() && coordinator.supportsHeartRateZones()) {
|
||||
List<HeartRateZones> zones = new ArrayList<>();
|
||||
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(HeartRateZonesSpec.PostureType.UPRIGHT);
|
||||
zones.add(loadOrCreateZones(HeartRateZonesSpec.PostureType.UPRIGHT, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
|
||||
if (coordinator.supportsExtendedHeartRateZones())
|
||||
zones.add(loadOrCreateZones(HeartRateZonesSpec.PostureType.UPRIGHT, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
|
||||
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.UPRIGHT,
|
||||
true,
|
||||
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
|
||||
HeartRateZones.CalculationMethod.MHR,
|
||||
zones
|
||||
));
|
||||
return res;
|
||||
}
|
||||
|
||||
if (!coordinator.supportsTrack())
|
||||
return null;
|
||||
|
||||
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.UPRIGHT,
|
||||
true,
|
||||
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
|
||||
HeartRateZones.CalculationMethod.MHR,
|
||||
getUprightZone(age)
|
||||
));
|
||||
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.SITTING,
|
||||
true,
|
||||
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
|
||||
HeartRateZones.CalculationMethod.MHR,
|
||||
getSittingZone(age)
|
||||
));
|
||||
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.SWIMMING,
|
||||
true,
|
||||
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
|
||||
HeartRateZones.CalculationMethod.MHR,
|
||||
getSwimmingZone(age)
|
||||
));
|
||||
res.add(loadOrCreateHeartRateZonesConfig(HeartRateZonesSpec.PostureType.OTHER,
|
||||
true,
|
||||
HeartRateZonesUtils.MAXIMUM_HEART_RATE - age,
|
||||
HeartRateZones.CalculationMethod.MHR,
|
||||
getOtherZone(age)
|
||||
));
|
||||
return res;
|
||||
}
|
||||
|
||||
private List<HeartRateZones> getUprightZone(int age) {
|
||||
List<HeartRateZones> res = new ArrayList<>();
|
||||
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(PostureType.UPRIGHT);
|
||||
res.add(loadOrCreateZones(PostureType.UPRIGHT, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
|
||||
res.add(loadOrCreateZones(PostureType.UPRIGHT, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
|
||||
res.add(loadOrCreateZones(PostureType.UPRIGHT, HeartRateZones.CalculationMethod.LTHR, maxHRThreshold));
|
||||
return res;
|
||||
}
|
||||
|
||||
private List<HeartRateZones> getSittingZone(int age) {
|
||||
List<HeartRateZones> res = new ArrayList<>();
|
||||
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(PostureType.SITTING);
|
||||
res.add(loadOrCreateZones(PostureType.SITTING, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
|
||||
res.add(loadOrCreateZones(PostureType.SITTING, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
|
||||
return res;
|
||||
}
|
||||
|
||||
private List<HeartRateZones> getSwimmingZone(int age) {
|
||||
List<HeartRateZones> res = new ArrayList<>();
|
||||
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(PostureType.SWIMMING);
|
||||
res.add(loadOrCreateZones(PostureType.SWIMMING, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
|
||||
res.add(loadOrCreateZones(PostureType.SWIMMING, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
|
||||
return res;
|
||||
}
|
||||
|
||||
private List<HeartRateZones> getOtherZone(int age) {
|
||||
List<HeartRateZones> res = new ArrayList<>();
|
||||
int maxHRThreshold = (HeartRateZonesUtils.MAXIMUM_HEART_RATE - age) - getHRCorrection(HeartRateZonesSpec.PostureType.OTHER);
|
||||
res.add(loadOrCreateZones(PostureType.OTHER, HeartRateZones.CalculationMethod.MHR, maxHRThreshold));
|
||||
res.add(loadOrCreateZones(PostureType.OTHER, HeartRateZones.CalculationMethod.HRR, maxHRThreshold));
|
||||
return res;
|
||||
}
|
||||
|
||||
//TODO: I am not sure about this. But it looks correct.
|
||||
private int getHRCorrection(HeartRateZonesSpec.PostureType type) {
|
||||
return switch (type) {
|
||||
case SITTING -> 6;
|
||||
case SWIMMING -> 10;
|
||||
case OTHER -> 5;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
public static HeartRateZonesConfig getByPosture(List<HeartRateZonesConfig> zones, HeartRateZonesSpec.PostureType type) {
|
||||
for (HeartRateZonesConfig cfg : zones) {
|
||||
if (cfg.getType() == type) {
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static HeartRateZones getByMethod(HeartRateZonesConfig cfg, HeartRateZones.CalculationMethod method) {
|
||||
for (HeartRateZones zn : cfg.getConfigByMethods()) {
|
||||
if (zn.getMethod() == method) {
|
||||
return zn;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static HeartRateZones getHRZonesConfigByPostureAndCalculationMethod(List<HeartRateZonesConfig> zones, HeartRateZonesSpec.PostureType type, HeartRateZones.CalculationMethod method) {
|
||||
|
||||
HeartRateZonesConfig cfg = getByPosture(zones, type);
|
||||
if (cfg == null) {
|
||||
cfg = getByPosture(zones, PostureType.UPRIGHT); // Use as default
|
||||
}
|
||||
if (cfg == null)
|
||||
return null;
|
||||
|
||||
return getByMethod(cfg, method);
|
||||
}
|
||||
|
||||
public static HeartRateZonesSpec.PostureType fromHuaweiPostureType(int type) {
|
||||
return switch (type) {
|
||||
case HUAWEI_TYPE_SITTING -> PostureType.SITTING;
|
||||
case HUAWEI_TYPE_SWIMMING -> PostureType.SWIMMING;
|
||||
case HUAWEI_TYPE_OTHER -> PostureType.OTHER;
|
||||
default -> PostureType.UPRIGHT;
|
||||
};
|
||||
}
|
||||
|
||||
public static int toHuaweiPostureType(HeartRateZonesSpec.PostureType type) {
|
||||
return switch (type) {
|
||||
case UPRIGHT -> HUAWEI_TYPE_UPRIGHT;
|
||||
case SITTING -> HUAWEI_TYPE_SITTING;
|
||||
case SWIMMING -> HUAWEI_TYPE_SWIMMING;
|
||||
case OTHER -> HUAWEI_TYPE_OTHER;
|
||||
};
|
||||
}
|
||||
|
||||
public static HeartRateZones.CalculationMethod fromHuaweiCalculationMethod(int type) {
|
||||
return switch (type) {
|
||||
case HUAWEI_CALCULATE_METHOD_MHR -> HeartRateZones.CalculationMethod.MHR;
|
||||
case HUAWEI_CALCULATE_METHOD_LTHR -> HeartRateZones.CalculationMethod.LTHR;
|
||||
default -> HeartRateZones.CalculationMethod.HRR;
|
||||
};
|
||||
}
|
||||
|
||||
public static int toHuaweiCalculationMethod(HeartRateZones.CalculationMethod type) {
|
||||
return switch (type) {
|
||||
case MHR -> HUAWEI_CALCULATE_METHOD_MHR;
|
||||
case HRR -> HUAWEI_CALCULATE_METHOD_HRR;
|
||||
case LTHR -> HUAWEI_CALCULATE_METHOD_LTHR;
|
||||
};
|
||||
}
|
||||
|
||||
public static int getZoneForHR(int heartRate, HeartRateZones zones) {
|
||||
return getZoneForHR(heartRate, zones.getZone5(), zones.getZone4(), zones.getZone3(), zones.getZone2(), zones.getZone1());
|
||||
}
|
||||
|
||||
private static int getZoneForHR(int heartRate, int zone5Threshold, int zone4Threshold, int zone3Threshold, int zone2Threshold, int zone1Threshold) {
|
||||
if (heartRate >= HeartRateZonesUtils.MAXIMUM_HEART_RATE) {
|
||||
return -1;
|
||||
}
|
||||
if (heartRate >= zone5Threshold) {
|
||||
return 4;
|
||||
}
|
||||
if (heartRate >= zone4Threshold) {
|
||||
return 3;
|
||||
}
|
||||
if (heartRate >= zone3Threshold) {
|
||||
return 2;
|
||||
}
|
||||
if (heartRate >= zone2Threshold) {
|
||||
return 1;
|
||||
}
|
||||
return heartRate >= zone1Threshold ? 0 : -1;
|
||||
}
|
||||
}
|
||||
+5
@@ -45,6 +45,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.SleepScoreSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Spo2Sample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.StressSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.TemperatureSample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiLESupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
@@ -323,4 +324,8 @@ public abstract class HuaweiLECoordinator extends AbstractBLEDeviceCoordinator i
|
||||
public boolean addBatteryPollingSettings() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public HeartRateZonesSpec getHeartRateZonesSpec(@NonNull GBDevice device) {
|
||||
return huaweiCoordinator.getHeartRateZonesSpec(device);
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -32,6 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateSettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.ui.HuaweiStressCalibrationActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiWorkoutGbParser;
|
||||
@@ -172,6 +173,16 @@ public class HuaweiSettingsCustomizer implements DeviceSpecificSettingsCustomize
|
||||
});
|
||||
}
|
||||
|
||||
final Preference hrSettings = handler.findPreference("pref_perform_heart_rate_settings");
|
||||
if (hrSettings != null) {
|
||||
hrSettings.setOnPreferenceClickListener(preference -> {
|
||||
final Intent intent = new Intent(handler.getContext(), HeartRateSettingsActivity.class);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, handler.getDevice());
|
||||
handler.getContext().startActivity(intent);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Huawei devices do not support lookahead > 7 days
|
||||
final Preference calendarLookahead = handler.findPreference(DeviceSettingsPreferenceConst.PREF_CALENDAR_LOOKAHEAD_DAYS);
|
||||
if (calendarLookahead != null) {
|
||||
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
/* Copyright (C) 2024 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+29
-30
@@ -20,11 +20,13 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiReportThreshold;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiRunPaceConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
|
||||
|
||||
public class FitnessData {
|
||||
|
||||
@@ -618,35 +620,32 @@ public class FitnessData {
|
||||
HuaweiTLV subTlv = new HuaweiTLV().
|
||||
put(0x08, heartRateZonesConfig.getWarningEnable());
|
||||
|
||||
if (
|
||||
heartRateZonesConfig.hasValidMHRData() &&
|
||||
heartRateZonesConfig.getWarningHRLimit() > 0 &&
|
||||
heartRateZonesConfig.getMaxHRThreshold() > 0
|
||||
) {
|
||||
HeartRateZones mhr = HuaweiHeartRateZonesSpec.getByMethod(heartRateZonesConfig, HeartRateZones.CalculationMethod.MHR);
|
||||
|
||||
if (mhr != null && mhr.hasValidData() && heartRateZonesConfig.getWarningHRLimit() > 0 && mhr.getHRThreshold() > 0) {
|
||||
subTlv
|
||||
.put(0x09, (byte) heartRateZonesConfig.getWarningHRLimit())
|
||||
.put(0x02, (byte) heartRateZonesConfig.getMHRWarmUp())
|
||||
.put(0x03, (byte) heartRateZonesConfig.getMHRFatBurning())
|
||||
.put(0x04, (byte) heartRateZonesConfig.getMHRAerobic())
|
||||
.put(0x05, (byte) heartRateZonesConfig.getMHRAnaerobic())
|
||||
.put(0x06, (byte) heartRateZonesConfig.getMHRExtreme())
|
||||
.put(0x07, (byte) heartRateZonesConfig.getMaxHRThreshold())
|
||||
.put(0x0b, (byte) heartRateZonesConfig.getMaxHRThreshold());
|
||||
.put(0x02, (byte) mhr.getZone1())
|
||||
.put(0x03, (byte) mhr.getZone2())
|
||||
.put(0x04, (byte) mhr.getZone3())
|
||||
.put(0x05, (byte) mhr.getZone4())
|
||||
.put(0x06, (byte) mhr.getZone5())
|
||||
.put(0x07, (byte) mhr.getHRThreshold())
|
||||
.put(0x0b, (byte) mhr.getHRThreshold());
|
||||
}
|
||||
|
||||
if (id == id_extended && heartRateZonesConfig.hasValidHRRData()) {
|
||||
subTlv
|
||||
.put(0x0d, (byte) heartRateZonesConfig.getHRRBasicAerobic())
|
||||
.put(0x0e, (byte) heartRateZonesConfig.getHRRAdvancedAerobic())
|
||||
.put(0x0f, (byte) heartRateZonesConfig.getHRRLactate())
|
||||
.put(0x10, (byte) heartRateZonesConfig.getHRRBasicAnaerobic())
|
||||
.put(0x11, (byte) heartRateZonesConfig.getHRRAdvancedAnaerobic());
|
||||
}
|
||||
|
||||
if (id == id_extended && heartRateZonesConfig.getRestHeartRate() > 0) {
|
||||
subTlv
|
||||
.put(0x0a, (byte) heartRateZonesConfig.getCalculateMethod())
|
||||
.put(0x0c, (byte) heartRateZonesConfig.getRestHeartRate());
|
||||
if (id == id_extended) {
|
||||
HeartRateZones hrr = HuaweiHeartRateZonesSpec.getByMethod(heartRateZonesConfig, HeartRateZones.CalculationMethod.HRR);
|
||||
if (hrr != null && hrr.hasValidData()) {
|
||||
subTlv
|
||||
.put(0x0d, (byte) hrr.getZone1())
|
||||
.put(0x0e, (byte) hrr.getZone2())
|
||||
.put(0x0f, (byte) hrr.getZone3())
|
||||
.put(0x10, (byte) hrr.getZone4())
|
||||
.put(0x11, (byte) hrr.getZone5())
|
||||
.put(0x0a, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(heartRateZonesConfig.getCurrentCalculationMethod()))
|
||||
.put(0x0c, (byte) hrr.getHRResting());
|
||||
}
|
||||
}
|
||||
|
||||
this.tlv = new HuaweiTLV().put(0x81, subTlv);
|
||||
@@ -732,8 +731,8 @@ public class FitnessData {
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, enabled);
|
||||
if(enabled)
|
||||
this.tlv.put(0x02, highHeartRateAlert);
|
||||
if (enabled)
|
||||
this.tlv.put(0x02, highHeartRateAlert);
|
||||
|
||||
this.isEncrypted = true;
|
||||
this.complete = true;
|
||||
@@ -753,7 +752,7 @@ public class FitnessData {
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, enabled);
|
||||
if(enabled)
|
||||
if (enabled)
|
||||
this.tlv.put(0x02, lowHeartRateAlert);
|
||||
|
||||
this.isEncrypted = true;
|
||||
@@ -811,7 +810,7 @@ public class FitnessData {
|
||||
|
||||
this.tlv = new HuaweiTLV()
|
||||
.put(0x01, enabled);
|
||||
if(enabled)
|
||||
if (enabled)
|
||||
this.tlv.put(0x02, lowHeartRateAlert);
|
||||
|
||||
this.isEncrypted = true;
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.model.heartratezones;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
|
||||
public abstract class HeartRateZones {
|
||||
|
||||
public enum CalculationMethod {
|
||||
MHR,
|
||||
HRR,
|
||||
LTHR,
|
||||
}
|
||||
|
||||
public static String methodToString(Context context, CalculationMethod method) {
|
||||
return switch (method) {
|
||||
case MHR -> context.getString(R.string.hr_settings_zones_calculation_method_mhr);
|
||||
case HRR -> context.getString(R.string.hr_settings_zones_calculation_method_hrr);
|
||||
case LTHR -> context.getString(R.string.hr_settings_zones_calculation_method_lthr);
|
||||
};
|
||||
}
|
||||
|
||||
protected final CalculationMethod method;
|
||||
|
||||
protected int HRThreshold;
|
||||
protected int HRResting;
|
||||
|
||||
protected int zone5;
|
||||
protected int zone4;
|
||||
protected int zone3;
|
||||
protected int zone2;
|
||||
protected int zone1;
|
||||
|
||||
public HeartRateZones(CalculationMethod method, int HRThreshold) {
|
||||
this.method = method;
|
||||
this.HRThreshold = HRThreshold;
|
||||
this.HRResting = HeartRateZonesUtils.DEFAULT_REST_HEART_RATE;
|
||||
reset();
|
||||
}
|
||||
|
||||
public abstract void reset();
|
||||
|
||||
public abstract long getPercentage(int zone);
|
||||
|
||||
public CalculationMethod getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public int getHRThreshold() {
|
||||
return HRThreshold;
|
||||
}
|
||||
|
||||
public void setHRThreshold(int HRThreshold) {
|
||||
this.HRThreshold = HRThreshold;
|
||||
}
|
||||
|
||||
public int getHRResting() {
|
||||
return HRResting;
|
||||
}
|
||||
|
||||
public void setHRResting(int HRResting) {
|
||||
this.HRResting = HRResting;
|
||||
}
|
||||
|
||||
public int getZone5() {
|
||||
return zone5;
|
||||
}
|
||||
|
||||
public void setZone5(int zone5) {
|
||||
this.zone5 = zone5;
|
||||
}
|
||||
|
||||
public int getZone4() {
|
||||
return zone4;
|
||||
}
|
||||
|
||||
public void setZone4(int zone4) {
|
||||
this.zone4 = zone4;
|
||||
}
|
||||
|
||||
public int getZone3() {
|
||||
return zone3;
|
||||
}
|
||||
|
||||
public void setZone3(int zone3) {
|
||||
this.zone3 = zone3;
|
||||
}
|
||||
|
||||
public int getZone2() {
|
||||
return zone2;
|
||||
}
|
||||
|
||||
public void setZone2(int zone2) {
|
||||
this.zone2 = zone2;
|
||||
}
|
||||
|
||||
public int getZone1() {
|
||||
return zone1;
|
||||
}
|
||||
|
||||
public void setZone1(int zone1) {
|
||||
this.zone1 = zone1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean isValid() {
|
||||
return HeartRateZonesUtils.checkValue(this.HRThreshold) &&
|
||||
HeartRateZonesUtils.checkValue(this.HRResting) &&
|
||||
HeartRateZonesUtils.checkValue(this.zone1) &&
|
||||
HeartRateZonesUtils.checkValue(this.zone2) &&
|
||||
HeartRateZonesUtils.checkValue(this.zone3) &&
|
||||
HeartRateZonesUtils.checkValue(this.zone4) &&
|
||||
HeartRateZonesUtils.checkValue(this.zone5) &&
|
||||
this.zone1 <= this.zone2 &&
|
||||
this.zone2 <= this.zone3 &&
|
||||
this.zone3 <= this.zone4 &&
|
||||
this.zone4 <= this.zone5;
|
||||
}
|
||||
|
||||
public boolean hasValidData() {
|
||||
boolean valid = this.zone1 > 0 && this.zone2 > 0 && this.zone3 > 0 && this.zone4 > 0 && this.zone5 > 0 && this.HRThreshold > 0;
|
||||
if (method == CalculationMethod.HRR) {
|
||||
valid &= this.HRResting >0;
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.model.heartratezones;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class HeartRateZonesConfig {
|
||||
|
||||
private final HeartRateZonesSpec.PostureType type;
|
||||
// warnings
|
||||
private boolean warningEnable;
|
||||
private int warningHRLimit;
|
||||
|
||||
private HeartRateZones.CalculationMethod currentCalculationMethod;
|
||||
|
||||
|
||||
private final List<HeartRateZones> configByMethods;
|
||||
|
||||
|
||||
public HeartRateZonesConfig(HeartRateZonesSpec.PostureType type, boolean warningEnable, int warningHRLimit, HeartRateZones.CalculationMethod currentCalculationMethod, List<HeartRateZones> configByMethods) {
|
||||
this.type = type;
|
||||
this.warningEnable = warningEnable;
|
||||
this.warningHRLimit = warningHRLimit;
|
||||
this.currentCalculationMethod = currentCalculationMethod;
|
||||
this.configByMethods = configByMethods;
|
||||
}
|
||||
|
||||
public HeartRateZonesSpec.PostureType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public boolean getWarningEnable() {
|
||||
return warningEnable;
|
||||
}
|
||||
|
||||
public void setWarningEnable(boolean warningEnable) {
|
||||
this.warningEnable = warningEnable;
|
||||
}
|
||||
|
||||
public int getWarningHRLimit() {
|
||||
return warningHRLimit;
|
||||
}
|
||||
|
||||
public void setWarningHRLimit(int warningHRLimit) {
|
||||
this.warningHRLimit = warningHRLimit;
|
||||
}
|
||||
|
||||
public HeartRateZones.CalculationMethod getCurrentCalculationMethod() {
|
||||
return currentCalculationMethod;
|
||||
}
|
||||
|
||||
public void setCurrentCalculationMethod(HeartRateZones.CalculationMethod currentCalculationMethod) {
|
||||
this.currentCalculationMethod = currentCalculationMethod;
|
||||
}
|
||||
|
||||
public List<HeartRateZones> getConfigByMethods() {
|
||||
return configByMethods;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
for(HeartRateZones zn: configByMethods) {
|
||||
zn.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
boolean valid = HeartRateZonesUtils.checkValue(this.warningHRLimit) && this.warningHRLimit > 0 && !configByMethods.isEmpty();
|
||||
for (HeartRateZones cfg : configByMethods) {
|
||||
valid &= cfg.isValid();
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/* Copyright (C) 2025 Me7c7
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.model.heartratezones;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
|
||||
|
||||
|
||||
public abstract class HeartRateZonesSpec {
|
||||
|
||||
public enum PostureType {
|
||||
UPRIGHT,
|
||||
SITTING,
|
||||
SWIMMING,
|
||||
OTHER
|
||||
}
|
||||
|
||||
public static final String HR_ZONES_PREF_KEY_PREFIX = "heart_rate_zones_";
|
||||
public static final String HR_ZONES_PREF_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_method_%s";
|
||||
public static final String HR_ZONES_PREF_WARNING_ENABLED_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_warning_enabled";
|
||||
public static final String HR_ZONES_PREF_HEART_RATE_LIMIT_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_heart_rate_limit";
|
||||
public static final String HR_ZONES_PREF_CALCULATE_METHOD_KEY = HR_ZONES_PREF_KEY_PREFIX + "posture_%s_calculate_method";
|
||||
|
||||
protected final GBDevice device;
|
||||
|
||||
public HeartRateZonesSpec(GBDevice device) {
|
||||
this.device = device;
|
||||
}
|
||||
|
||||
|
||||
public abstract String getNameByType(Context context, PostureType type);
|
||||
|
||||
public abstract List<HeartRateZonesConfig> getDeviceConfig();
|
||||
|
||||
|
||||
private String getPrefKey(HeartRateZonesSpec.PostureType type, HeartRateZones.CalculationMethod method) {
|
||||
return String.format(Locale.ROOT, HR_ZONES_PREF_KEY, type.toString(), method.toString());
|
||||
}
|
||||
|
||||
protected String getHrZoneConfig(PostureType type, HeartRateZones.CalculationMethod method) {
|
||||
DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
|
||||
String key = getPrefKey(type, method);
|
||||
return devicePreferences.getString(key, null);
|
||||
}
|
||||
|
||||
protected HeartRateZonesConfig loadOrCreateHeartRateZonesConfig(PostureType type, boolean warningEnable, int warningHRLimit, HeartRateZones.CalculationMethod currentCalculationMethod, List<HeartRateZones> configByMethods) {
|
||||
DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
|
||||
|
||||
warningEnable = devicePreferences.getBoolean(String.format(Locale.ROOT, HR_ZONES_PREF_WARNING_ENABLED_KEY, type.toString()), warningEnable);
|
||||
warningHRLimit = devicePreferences.getInt(String.format(Locale.ROOT, HR_ZONES_PREF_HEART_RATE_LIMIT_KEY, type.toString()), warningHRLimit);
|
||||
HeartRateZones.CalculationMethod[] values = HeartRateZones.CalculationMethod.values();
|
||||
int val = devicePreferences.getInt(String.format(Locale.ROOT, HR_ZONES_PREF_CALCULATE_METHOD_KEY, type.toString()), currentCalculationMethod.ordinal());
|
||||
if (val >= 0 && val < values.length) {
|
||||
currentCalculationMethod = values[val];
|
||||
}
|
||||
return new HeartRateZonesConfig(type,
|
||||
warningEnable,
|
||||
warningHRLimit,
|
||||
currentCalculationMethod,
|
||||
configByMethods
|
||||
);
|
||||
}
|
||||
|
||||
public void saveConfig(HeartRateZonesConfig config) {
|
||||
final DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
|
||||
final SharedPreferences.Editor editor = devicePreferences.getPreferences().edit();
|
||||
|
||||
editor.putBoolean(String.format(Locale.ROOT, HR_ZONES_PREF_WARNING_ENABLED_KEY, config.getType().toString()), config.getWarningEnable());
|
||||
editor.putInt(String.format(Locale.ROOT, HR_ZONES_PREF_HEART_RATE_LIMIT_KEY, config.getType().toString()), config.getWarningHRLimit());
|
||||
editor.putInt(String.format(Locale.ROOT, HR_ZONES_PREF_CALCULATE_METHOD_KEY, config.getType().toString()), config.getCurrentCalculationMethod().ordinal());
|
||||
|
||||
Gson gson = new Gson();
|
||||
for (HeartRateZones zn : config.getConfigByMethods()) {
|
||||
String key = getPrefKey(config.getType(), zn.getMethod());
|
||||
editor.putString(key, gson.toJson(zn));
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
public void clearConfig() {
|
||||
final DevicePrefs devicePreferences = GBApplication.getDevicePrefs(device);
|
||||
SharedPreferences sharedPrefs = devicePreferences.getPreferences();
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
Map<String, ?> allEntries = sharedPrefs.getAll();
|
||||
for (Map.Entry<String, ?> entry : allEntries.entrySet()) {
|
||||
if(entry.getKey().startsWith(HR_ZONES_PREF_KEY_PREFIX)) {
|
||||
editor.remove(entry.getKey());
|
||||
}
|
||||
}
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.model.heartratezones;
|
||||
|
||||
public class HeartRateZonesUtils {
|
||||
public static final int MAXIMUM_HEART_RATE = 220;
|
||||
public static final int DEFAULT_REST_HEART_RATE = 60;
|
||||
|
||||
public static boolean checkValue(int val) {
|
||||
return val >= 0 && val < MAXIMUM_HEART_RATE;
|
||||
}
|
||||
}
|
||||
+8
-9
@@ -52,8 +52,7 @@ import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.Activity
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.workouts.entries.ActivitySummaryValue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSportHRZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Workout;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.BaseActivitySummary;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
@@ -75,8 +74,9 @@ import nodomain.freeyourgadget.gadgetbridge.model.ActivityPoint;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryEntries;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySummaryParser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.workout.WorkoutChart;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
@@ -798,7 +798,7 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
|
||||
|
||||
//NOTE: The method of retrieving HR zones from the Huawei watch is not discovered. It may not return zones.
|
||||
// So they are calculated based on config.
|
||||
HeartRateZonesConfig HRZonesCfg = null;
|
||||
HeartRateZones HRZonesCfg = null;
|
||||
|
||||
Integer zonePostureType = parseAndValidatePostureType(additionalValues.get("postureType"));
|
||||
LOG.info("Workout HR Zone Workout Posture Type: {}", zonePostureType);
|
||||
@@ -809,10 +809,9 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
|
||||
|
||||
int zoneCalculateMethod = summary.getHrZoneType();
|
||||
LOG.info("Workout HR Zone Calculate Type: {}", zoneCalculateMethod);
|
||||
if (zonePostureType != null && HeartRateZonesConfig.isCalculateMethodValidForPostureType(zonePostureType, zoneCalculateMethod)) {
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
HuaweiSportHRZones hrSportZones = new HuaweiSportHRZones(activityUser.getAge());
|
||||
HRZonesCfg = hrSportZones.getHRZonesConfigByType(zonePostureType);
|
||||
if (zonePostureType != null) {
|
||||
HeartRateZonesSpec spec = gbDevice.getDeviceCoordinator().getHeartRateZonesSpec(gbDevice);
|
||||
HRZonesCfg = HuaweiHeartRateZonesSpec.getHRZonesConfigByPostureAndCalculationMethod(spec.getDeviceConfig(), HuaweiHeartRateZonesSpec.fromHuaweiPostureType(zonePostureType), HuaweiHeartRateZonesSpec.fromHuaweiCalculationMethod(zoneCalculateMethod));
|
||||
}
|
||||
|
||||
int dataDelta = 5;
|
||||
@@ -830,7 +829,7 @@ public class HuaweiWorkoutGbParser implements ActivitySummaryParser {
|
||||
|
||||
|
||||
if (HRZonesCfg != null) {
|
||||
int zoneIdx = HRZonesCfg.getZoneByMethod(dataSample.getHeartRate() & 0xFF, zoneCalculateMethod);
|
||||
int zoneIdx = HuaweiHeartRateZonesSpec.getZoneForHR(dataSample.getHeartRate() & 0xFF, HRZonesCfg);
|
||||
if (zoneIdx != -1 && dataIdx < (dataSamples.size() - 1)) {
|
||||
HRZones[zoneIdx] += dataDelta;
|
||||
}
|
||||
|
||||
+22
-22
@@ -20,7 +20,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class HuaweiWorkoutUtils {
|
||||
@@ -29,27 +29,27 @@ public class HuaweiWorkoutUtils {
|
||||
//TODO: discover and add more activity types. Should be same as in the watch.
|
||||
private static Map<ActivityKind, Integer> createActivityHRZoneType() {
|
||||
final Map<ActivityKind, Integer> result = new HashMap<>();
|
||||
result.put(ActivityKind.RUNNING, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.WALKING, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.CYCLING, HeartRateZonesConfig.TYPE_SITTING);
|
||||
result.put(ActivityKind.MOUNTAIN_HIKE, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.INDOOR_RUNNING, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.POOL_SWIM, HeartRateZonesConfig.TYPE_SWIMMING);
|
||||
result.put(ActivityKind.INDOOR_CYCLING, HeartRateZonesConfig.TYPE_SITTING);
|
||||
result.put(ActivityKind.SWIMMING_OPENWATER, HeartRateZonesConfig.TYPE_SWIMMING);
|
||||
result.put(ActivityKind.INDOOR_WALKING, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.HIKING, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.JUMP_ROPING, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.PINGPONG, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.BADMINTON, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.TENNIS, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.SOCCER, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.BASKETBALL, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.VOLLEYBALL, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.ELLIPTICAL_TRAINER, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.ROWING_MACHINE, HeartRateZonesConfig.TYPE_SITTING);
|
||||
result.put(ActivityKind.STEPPER, HeartRateZonesConfig.TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.YOGA, HeartRateZonesConfig.TYPE_OTHER);
|
||||
result.put(ActivityKind.RUNNING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.WALKING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.CYCLING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SITTING);
|
||||
result.put(ActivityKind.MOUNTAIN_HIKE, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.INDOOR_RUNNING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.POOL_SWIM, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SWIMMING);
|
||||
result.put(ActivityKind.INDOOR_CYCLING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SITTING);
|
||||
result.put(ActivityKind.SWIMMING_OPENWATER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SWIMMING);
|
||||
result.put(ActivityKind.INDOOR_WALKING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.HIKING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.JUMP_ROPING, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.PINGPONG, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.BADMINTON, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.TENNIS, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.SOCCER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.BASKETBALL, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.VOLLEYBALL, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.ELLIPTICAL_TRAINER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.ROWING_MACHINE, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_SITTING);
|
||||
result.put(ActivityKind.STEPPER, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_UPRIGHT);
|
||||
result.put(ActivityKind.YOGA, HuaweiHeartRateZonesSpec.HUAWEI_TYPE_OTHER);
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
|
||||
+151
-8
@@ -21,10 +21,15 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiSportHRZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZones;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiP2PManager;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class HuaweiP2PTrackService extends HuaweiBaseP2PService {
|
||||
private final Logger LOG = LoggerFactory.getLogger(HuaweiP2PTrackService.class);
|
||||
@@ -70,13 +75,151 @@ public class HuaweiP2PTrackService extends HuaweiBaseP2PService {
|
||||
return "SystemApp";
|
||||
}
|
||||
|
||||
private boolean isNotValid(HeartRateZonesConfig cfg) {
|
||||
if(cfg == null)
|
||||
return true;
|
||||
return !cfg.isValid();
|
||||
}
|
||||
|
||||
private boolean isNotValidZone(HeartRateZones cfg) {
|
||||
if(cfg == null)
|
||||
return true;
|
||||
return !cfg.isValid();
|
||||
}
|
||||
|
||||
public byte[] getHRZonesData() {
|
||||
|
||||
HuaweiHeartRateZonesSpec spec = new HuaweiHeartRateZonesSpec(manager.getSupportProvider().getDevice(), manager.getSupportProvider().getHuaweiCoordinator());
|
||||
List<HeartRateZonesConfig> zones = spec.getDeviceConfig();
|
||||
HeartRateZonesConfig uprightConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.UPRIGHT);
|
||||
HeartRateZonesConfig sittingConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.SITTING);
|
||||
HeartRateZonesConfig swimmingConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.SWIMMING);
|
||||
HeartRateZonesConfig otherConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.OTHER);
|
||||
|
||||
if (isNotValid(uprightConfig) || isNotValid(sittingConfig) || isNotValid(swimmingConfig) || isNotValid(otherConfig)) {
|
||||
return null;
|
||||
}
|
||||
HeartRateZones uprightMhr = HuaweiHeartRateZonesSpec.getByMethod(uprightConfig, HeartRateZones.CalculationMethod.MHR);
|
||||
HeartRateZones uprightHrr = HuaweiHeartRateZonesSpec.getByMethod(uprightConfig, HeartRateZones.CalculationMethod.HRR);
|
||||
HeartRateZones uprightLthr = HuaweiHeartRateZonesSpec.getByMethod(uprightConfig, HeartRateZones.CalculationMethod.LTHR);
|
||||
if(isNotValidZone(uprightMhr) || isNotValidZone(uprightHrr) || isNotValidZone(uprightLthr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HeartRateZones sittingMhr = HuaweiHeartRateZonesSpec.getByMethod(sittingConfig, HeartRateZones.CalculationMethod.MHR);
|
||||
HeartRateZones sittingHrr = HuaweiHeartRateZonesSpec.getByMethod(sittingConfig, HeartRateZones.CalculationMethod.HRR);
|
||||
if(isNotValidZone(sittingMhr) || isNotValidZone(sittingHrr)) {
|
||||
return null;
|
||||
}
|
||||
HeartRateZones swimmingMhr = HuaweiHeartRateZonesSpec.getByMethod(swimmingConfig, HeartRateZones.CalculationMethod.MHR);
|
||||
HeartRateZones swimmingHrr = HuaweiHeartRateZonesSpec.getByMethod(swimmingConfig, HeartRateZones.CalculationMethod.HRR);
|
||||
if(isNotValidZone(swimmingMhr) || isNotValidZone(swimmingHrr)) {
|
||||
return null;
|
||||
}
|
||||
HeartRateZones otherMhr = HuaweiHeartRateZonesSpec.getByMethod(otherConfig, HeartRateZones.CalculationMethod.MHR);
|
||||
HeartRateZones otherHrr = HuaweiHeartRateZonesSpec.getByMethod(otherConfig, HeartRateZones.CalculationMethod.HRR);
|
||||
if(isNotValidZone(otherMhr) || isNotValidZone(otherHrr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
HuaweiTLV tlv = new HuaweiTLV();
|
||||
|
||||
if (uprightMhr.hasValidData()) {
|
||||
tlv.put(0x2, (byte) uprightMhr.getZone1())
|
||||
.put(0x3, (byte) uprightMhr.getZone2())
|
||||
.put(0x4, (byte) uprightMhr.getZone3())
|
||||
.put(0x5, (byte) uprightMhr.getZone4())
|
||||
.put(0x6, (byte) uprightMhr.getZone5())
|
||||
.put(0x7, (byte) uprightMhr.getHRThreshold())
|
||||
.put(0x8, (byte) (uprightConfig.getWarningEnable() ? 1 : 0))
|
||||
.put(0x9, (byte) uprightConfig.getWarningHRLimit())
|
||||
.put(0xa, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(uprightConfig.getCurrentCalculationMethod()))
|
||||
.put(0xb, (byte) uprightMhr.getHRThreshold());
|
||||
}
|
||||
tlv.put(0xc, (byte) uprightHrr.getHRResting());
|
||||
if (uprightHrr.hasValidData()) {
|
||||
tlv.put(0xd, (byte) uprightHrr.getZone1())
|
||||
.put(0xe, (byte) uprightHrr.getZone2())
|
||||
.put(0xf, (byte) uprightHrr.getZone3())
|
||||
.put(0x10, (byte) uprightHrr.getZone4())
|
||||
.put(0x11, (byte) uprightHrr.getZone5());
|
||||
|
||||
}
|
||||
if (uprightLthr.hasValidData()) {
|
||||
tlv.put(0x3f, (byte) uprightLthr.getHRThreshold())
|
||||
.put(0x40, (byte) uprightLthr.getZone5())
|
||||
.put(0x41, (byte) uprightLthr.getZone4())
|
||||
.put(0x42, (byte) uprightLthr.getZone3())
|
||||
.put(0x43, (byte) uprightLthr.getZone2())
|
||||
.put(0x44, (byte) uprightLthr.getZone1());
|
||||
}
|
||||
|
||||
if (sittingMhr.hasValidData()) {
|
||||
tlv.put(0x12, (byte) (sittingConfig.getWarningEnable() ? 1 : 0))
|
||||
.put(0x13, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(sittingConfig.getCurrentCalculationMethod()))
|
||||
.put(0x14, (byte) sittingConfig.getWarningHRLimit())
|
||||
.put(0x15, (byte) sittingMhr.getZone1())
|
||||
.put(0x16, (byte) sittingMhr.getZone2())
|
||||
.put(0x17, (byte) sittingMhr.getZone3())
|
||||
.put(0x18, (byte) sittingMhr.getZone4())
|
||||
.put(0x19, (byte) sittingMhr.getZone5())
|
||||
.put(0x1a, (byte) sittingMhr.getHRThreshold());
|
||||
}
|
||||
if (sittingHrr.hasValidData()) {
|
||||
tlv.put(0x1b, (byte) sittingHrr.getHRResting())
|
||||
.put(0x1c, (byte) sittingHrr.getZone1())
|
||||
.put(0x1d, (byte) sittingHrr.getZone2())
|
||||
.put(0x1e, (byte) sittingHrr.getZone3())
|
||||
.put(0x1f, (byte) sittingHrr.getZone4())
|
||||
.put(0x20, (byte) sittingHrr.getZone5());
|
||||
}
|
||||
|
||||
if (swimmingMhr.hasValidData()) {
|
||||
tlv.put(0x21, (byte) (swimmingConfig.getWarningEnable() ? 1 : 0))
|
||||
.put(0x22, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(swimmingConfig.getCurrentCalculationMethod()))
|
||||
.put(0x23, (byte) swimmingConfig.getWarningHRLimit())
|
||||
.put(0x24, (byte) swimmingMhr.getZone1())
|
||||
.put(0x25, (byte) swimmingMhr.getZone2())
|
||||
.put(0x26, (byte) swimmingMhr.getZone3())
|
||||
.put(0x27, (byte) swimmingMhr.getZone4())
|
||||
.put(0x28, (byte) swimmingMhr.getZone5())
|
||||
.put(0x29, (byte) swimmingMhr.getHRThreshold());
|
||||
}
|
||||
if (swimmingHrr.hasValidData()) {
|
||||
tlv.put(0x2a, (byte) swimmingHrr.getHRResting())
|
||||
.put(0x2b, (byte) swimmingHrr.getZone1())
|
||||
.put(0x2c, (byte) swimmingHrr.getZone2())
|
||||
.put(0x2d, (byte) swimmingHrr.getZone3())
|
||||
.put(0x2e, (byte) swimmingHrr.getZone4())
|
||||
.put(0x2f, (byte) swimmingHrr.getZone5());
|
||||
}
|
||||
|
||||
if (otherMhr.hasValidData()) {
|
||||
tlv.put(0x30, (byte) (otherConfig.getWarningEnable() ? 1 : 0))
|
||||
.put(0x31, (byte) HuaweiHeartRateZonesSpec.toHuaweiCalculationMethod(otherConfig.getCurrentCalculationMethod()))
|
||||
.put(0x32, (byte) otherConfig.getWarningHRLimit())
|
||||
.put(0x33, (byte) otherMhr.getZone1())
|
||||
.put(0x34, (byte) otherMhr.getZone2())
|
||||
.put(0x35, (byte) otherMhr.getZone3())
|
||||
.put(0x36, (byte) otherMhr.getZone4())
|
||||
.put(0x37, (byte) otherMhr.getZone5())
|
||||
.put(0x38, (byte) otherMhr.getHRThreshold());
|
||||
}
|
||||
if (otherHrr.hasValidData()) {
|
||||
tlv.put(0x39, (byte) otherHrr.getHRResting())
|
||||
.put(0x3a, (byte) otherHrr.getZone1())
|
||||
.put(0x3b, (byte) otherHrr.getZone2())
|
||||
.put(0x3c, (byte) otherHrr.getZone3())
|
||||
.put(0x3d, (byte) otherHrr.getZone4())
|
||||
.put(0x3e, (byte) otherHrr.getZone5());
|
||||
}
|
||||
|
||||
return tlv.serialize();
|
||||
}
|
||||
|
||||
public void sendHeartZoneConfig() {
|
||||
|
||||
ActivityUser activityUser = new ActivityUser();
|
||||
|
||||
HuaweiSportHRZones hrZones = new HuaweiSportHRZones(activityUser.getAge());
|
||||
|
||||
byte[] data = hrZones.getHRZonesData();
|
||||
byte[] data = getHRZonesData();
|
||||
if (data == null) {
|
||||
LOG.error("Incorrect Heart Rate config");
|
||||
return;
|
||||
|
||||
+6
-3
@@ -18,10 +18,11 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiHeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.FitnessData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.heartratezones.HeartRateZonesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendHeartRateZonesConfig extends Request {
|
||||
@@ -44,7 +45,9 @@ public class SendHeartRateZonesConfig extends Request {
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
HeartRateZonesConfig heartRateZonesConfig = new HeartRateZonesConfig(HeartRateZonesConfig.TYPE_UPRIGHT, new ActivityUser().getAge());
|
||||
HuaweiHeartRateZonesSpec spec = new HuaweiHeartRateZonesSpec(supportProvider.getDevice(), supportProvider.getHuaweiCoordinator());
|
||||
List<HeartRateZonesConfig> zones = spec.getDeviceConfig();
|
||||
HeartRateZonesConfig heartRateZonesConfig = HuaweiHeartRateZonesSpec.getByPosture(zones, HeartRateZonesSpec.PostureType.UPRIGHT);
|
||||
if (supportProvider.getHuaweiCoordinator().supportsExtendedHeartRateZones()) {
|
||||
return FitnessData.HeartRateZoneConfigPacket.Request.requestExtended(paramsProvider, heartRateZonesConfig).serialize();
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/hr_settings_tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/hr_settings_limits"
|
||||
layout="@layout/fragment_heart_rate_settings_limit" />
|
||||
|
||||
<include
|
||||
android:id="@+id/hr_settings_zones"
|
||||
layout="@layout/fragment_heart_rate_settings_zones" />
|
||||
<Button
|
||||
android:id="@+id/save_hr_zones_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/save_configuration" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/pref_heart_rate_high_alert" />
|
||||
|
||||
<com.google.android.material.materialswitch.MaterialSwitch
|
||||
android:id="@+id/high_hr_warning"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:checked="false" />
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/hr_settings_hr_limit" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/limit_high_hr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
@@ -0,0 +1,402 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout 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="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/hr_settings_zones_calculation_method" />
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/calculation_method_spinner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/zones_max_hr_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/hr_settings_zones_max_hr" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/zones_max_hr"
|
||||
android:gravity="end"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_zones_resting_hr"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/hr_settings_zones_resting_hr" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/zones_resting_hr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hr_zone_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="5dp"
|
||||
android:gravity="center"
|
||||
android:minHeight="10dp"
|
||||
android:text="Zone"
|
||||
android:textColor="#777777"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hr_value_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="5sp"
|
||||
android:gravity="center"
|
||||
android:minWidth="100dp"
|
||||
android:minHeight="15dp"
|
||||
android:text="@string/heart_rate"
|
||||
android:textColor="#777777"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/hr_percentage_title"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hr_percentage_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:minWidth="100dp"
|
||||
android:minHeight="15dp"
|
||||
android:text="%"
|
||||
android:textColor="#777777"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_hr_zone_5"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@+id/heart_rate_zone_5_divider"
|
||||
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_4"
|
||||
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_5_divider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/layout_hr_zone_values">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_heart_rate_zone_5"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Zone 5" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_hr_zone_4"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@+id/heart_rate_zone_4_divider"
|
||||
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_3"
|
||||
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_4_divider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_5">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_heart_rate_zone_4"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Zone 4" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_hr_zone_3"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@+id/heart_rate_zone_3_divider"
|
||||
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_2"
|
||||
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_3_divider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_4">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_heart_rate_zone_3"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Zone 3" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_hr_zone_2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@+id/heart_rate_zone_2_divider"
|
||||
app:layout_constraintBottom_toTopOf="@+id/layout_hr_zone_1"
|
||||
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_2_divider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_3">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_heart_rate_zone_2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Zone 2" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/layout_hr_zone_1"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@+id/heart_rate_zone_1_divider"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/layout_hr_zone_values"
|
||||
app:layout_constraintEnd_toStartOf="@+id/heart_rate_zone_1_divider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/layout_hr_zone_2">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_heart_rate_zone_1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Zone 1" />
|
||||
</RelativeLayout>
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
|
||||
android:id="@+id/heart_rate_zone_5_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="25dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_4_divider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
|
||||
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_5"
|
||||
app:layout_constraintTop_toTopOf="@+id/layout_hr_zone_values" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
|
||||
android:id="@+id/heart_rate_zone_4_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_3_divider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
|
||||
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_4"
|
||||
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_5_divider" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
|
||||
android:id="@+id/heart_rate_zone_3_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_2_divider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
|
||||
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_3"
|
||||
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_4_divider" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
|
||||
android:id="@+id/heart_rate_zone_2_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/heart_rate_zone_1_divider"
|
||||
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
|
||||
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_2"
|
||||
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_3_divider" />
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.activities.heartratezones.HeartRateZonesDividerView
|
||||
android:id="@+id/heart_rate_zone_1_divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginBottom="25dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/layout_hr_zone_values"
|
||||
app:layout_constraintEnd_toStartOf="@+id/layout_hr_zone_values"
|
||||
app:layout_constraintStart_toEndOf="@+id/layout_hr_zone_1"
|
||||
app:layout_constraintTop_toBottomOf="@+id/heart_rate_zone_2_divider" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_hr_zone_values"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toStartOf="@+id/hr_percentage_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/hr_value_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/hr_percentage_title">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_max_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_5_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_4_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_3_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_2_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_1_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_pace_range_percentage"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="5sp"
|
||||
android:layout_marginEnd="6sp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="@+id/hr_percentage_title"
|
||||
app:layout_constraintStart_toStartOf="@+id/hr_percentage_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/hr_percentage_title">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_max_percent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_5_percent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_4_percent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_3_percent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_2_percent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/heart_rate_zone_1_percent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="35dp"
|
||||
android:layout_marginTop="28dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/n_a" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".activities.heartratezones.HeartRateSettingsFragment">
|
||||
<item
|
||||
android:id="@+id/hr_zones_reset_all"
|
||||
android:icon="@drawable/ic_delete_forever"
|
||||
android:title="@string/hr_settings_zones_remove_all"
|
||||
app:iconTint="?attr/actionmenu_icon_color"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
@@ -4356,4 +4356,18 @@
|
||||
<string name="huawei_send_gps_and_time_summary">Send GPS and Time to device for weather and search stars optimization. Location should be set in the settings</string>
|
||||
<string name="garmin_mlr_protocol">Multi-Link Reliable Protocol</string>
|
||||
<string name="feature_experimental_unstable">Experimental, might be unstable</string>
|
||||
<string name="heart_rate_settings">Heart rate settings</string>
|
||||
<string name="heart_rate_settings_summary">View current workout heart rate settings and zones</string>
|
||||
<string name="workout_posture_type_upright">Upright</string>
|
||||
<string name="workout_posture_type_sitting">Sitting</string>
|
||||
<string name="hr_settings_hr_limit">Heart rate limit</string>
|
||||
<string name="hr_settings_zones_calculation_method">Calculation method</string>
|
||||
<string name="hr_settings_zones_calculation_method_mhr">MHR</string>
|
||||
<string name="hr_settings_zones_calculation_method_hrr">HRR</string>
|
||||
<string name="hr_settings_zones_calculation_method_lthr">LTHR</string>
|
||||
<string name="hr_settings_zones_resting_hr">Resting heart rate</string>
|
||||
<string name="hr_settings_zones_max_hr">Maximum heart rate</string>
|
||||
<string name="hr_settings_zones_lactate_threshold_hr">Lactate threshold heart rate</string>
|
||||
<string name="hr_settings_zones_remove_all">Reset all</string>
|
||||
<string name="hr_settings_zones_remove_all_description">Are you sure you want to delete the heart rate zones and limits for all posture configurations and reset them to default?</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<Preference
|
||||
android:key="pref_perform_heart_rate_settings"
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:title="@string/heart_rate_settings"
|
||||
android:summary="@string/heart_rate_settings_summary">
|
||||
</Preference>
|
||||
</androidx.preference.PreferenceScreen>
|
||||
Reference in New Issue
Block a user