mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Garmin: HRM-Pro Plus - native support (#5353)
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
||||
/* Copyright (C) 2025 Thomas Kuehne
|
||||
|
||||
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.garmin.hrm;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.GarminSupportHrm;
|
||||
|
||||
public class GarminHrmProPlusCoordinator extends GarminCoordinator {
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_lovetoy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_hrm_pro_plus;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass(final GBDevice device) {
|
||||
return GarminSupportHrm.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^HRMPro[+]:\\d+$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExperimental() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean suggestUnbindBeforePair() {
|
||||
// Not needed
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching(@NonNull final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking(@NonNull final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(@NonNull final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsManualHeartRateMeasurement(@NonNull final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData(@NonNull final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSleepMeasurement(@NonNull final GBDevice device){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.galaxy_buds.GalaxyBudsProDev
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.bike.GarminEdge130PlusCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.bike.GarminEdge540Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.bike.GarminEdgeExplore2Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.hrm.GarminHrmProPlusCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.gps.GarminETrexSeCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.gps.GarminInReachMini2;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.hrm.GarminHrm200Coordinator;
|
||||
@@ -562,6 +563,7 @@ public enum DeviceType {
|
||||
IKEA_IDASEN(IdasenCoordinator.class),
|
||||
NUTMINI(NutCoordinator.class),
|
||||
VIVOMOVE_HR(GarminVivomoveHrCoordinator.class),
|
||||
GARMIN_HRM_PRO_PLUS(GarminHrmProPlusCoordinator.class),
|
||||
GARMIN_HRM_200(GarminHrm200Coordinator.class),
|
||||
GARMIN_EDGE_130_PLUS(GarminEdge130PlusCoordinator.class),
|
||||
GARMIN_EDGE_540(GarminEdge540Coordinator.class),
|
||||
|
||||
+19
-2
@@ -1,5 +1,5 @@
|
||||
/* Copyright (C) 2016-2024 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti
|
||||
/* Copyright (C) 2016-2025 Andreas Shimokawa, Carsten Pfeiffer, Daniele
|
||||
Gobbetti, Thomas Kuehne
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
@@ -27,11 +27,13 @@ import org.slf4j.LoggerFactory;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.AbstractBleProfile;
|
||||
|
||||
/**
|
||||
* https://www.bluetooth.com/specifications/gatt/viewer?attributeXmlFile=org.bluetooth.service.heart_rate.xml
|
||||
* @see GattService#UUID_SERVICE_HEART_RATE
|
||||
*/
|
||||
public class HeartRateProfile<T extends AbstractBTLESingleDeviceSupport> extends AbstractBleProfile<T> {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HeartRateProfile.class);
|
||||
@@ -84,6 +86,21 @@ public class HeartRateProfile<T extends AbstractBTLESingleDeviceSupport> extends
|
||||
heartRate = BLETypeConversions.toUnsigned(value, 1);
|
||||
}
|
||||
|
||||
if ((flag & 0x04) != 0){
|
||||
// Sensor Contact supported
|
||||
if ((flag & 0x02) == 0){
|
||||
// Sensor Contact NOT detected - no or poor contact with the skin
|
||||
LOG.debug("Got poor contact heartRate: {}", heartRate);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if ((flag & 0x08) != 0){
|
||||
// TODO: Energy Expended present (UINT16, unit: kilo Joules since last reset)
|
||||
}
|
||||
if ((flag & 0x10) != 0){
|
||||
// TODO: RR-Interval present (UINT16 array, unit: 1/1024 second)
|
||||
}
|
||||
|
||||
LOG.debug("Got heartRate: {}", heartRate);
|
||||
|
||||
notify(createIntent(heartRate));
|
||||
|
||||
+1
@@ -52,6 +52,7 @@ public class FileType {
|
||||
GOALS(128, 11),
|
||||
SUMMARY(128, 20),
|
||||
RECORDS(128, 29),
|
||||
UNKNOWN_31(128, 31), // sent by HRM Pro Plus
|
||||
MONITOR(128, 32),
|
||||
MLT_SPORT(128, 33),
|
||||
SEGMENTS(128, 34),
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/* Copyright (C) 2025 Thomas Kuehne
|
||||
|
||||
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.service.devices.garmin;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminActivitySampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.GarminActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattCharacteristic;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.IntentListener;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.battery.BatteryInfoProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.heartrate.HeartRateProfile;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class GarminSupportHrm extends GarminSupport {
|
||||
private final BatteryInfoProfile<GarminSupportHrm> batteryInfoProfile;
|
||||
private final HeartRateProfile<GarminSupportHrm> heartRateProfile;
|
||||
|
||||
public GarminSupportHrm() {
|
||||
addSupportedService(BatteryInfoProfile.SERVICE_UUID);
|
||||
batteryInfoProfile = new BatteryInfoProfile<>(this);
|
||||
batteryInfoProfile.addListener(new BatteryListener());
|
||||
addSupportedProfile(batteryInfoProfile);
|
||||
|
||||
addSupportedService(GattService.UUID_SERVICE_HEART_RATE);
|
||||
heartRateProfile = new HeartRateProfile<>(this);
|
||||
heartRateProfile.addListener(new HeartRateListener());
|
||||
addSupportedProfile(heartRateProfile);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
|
||||
super.initializeDevice(builder);
|
||||
|
||||
if (null != getCharacteristic(BatteryInfoProfile.UUID_CHARACTERISTIC_BATTERY_LEVEL)) {
|
||||
batteryInfoProfile.requestBatteryInfo(builder);
|
||||
batteryInfoProfile.enableNotify(builder, true);
|
||||
|
||||
}
|
||||
|
||||
if (null != getCharacteristic(GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT)) {
|
||||
heartRateProfile.enableNotify(builder, true);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
final class BatteryListener implements IntentListener {
|
||||
@Override
|
||||
public void notify(Intent intent) {
|
||||
BatteryInfo info = intent.getParcelableExtra(BatteryInfoProfile.EXTRA_BATTERY_INFO);
|
||||
if (info != null) {
|
||||
GBDeviceEventBatteryInfo event = new GBDeviceEventBatteryInfo();
|
||||
event.state = BatteryState.BATTERY_NORMAL;
|
||||
event.level = info.getPercentCharged();
|
||||
handleGBDeviceEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
// noop - device publishes HR ca every second
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(final boolean enable) {
|
||||
// noop - HR is always enabled
|
||||
}
|
||||
|
||||
final class HeartRateListener implements IntentListener {
|
||||
@Override
|
||||
public void notify(Intent intent) {
|
||||
int hr = intent.getIntExtra(HeartRateProfile.EXTRA_HEART_RATE, -1);
|
||||
if (hr > 0) {
|
||||
final GarminActivitySample sample;
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
final DaoSession session = handler.getDaoSession();
|
||||
final GarminActivitySampleProvider provider = new GarminActivitySampleProvider(getDevice(), session);
|
||||
|
||||
final Device device = DBHelper.getDevice(gbDevice, session);
|
||||
final User user = DBHelper.getUser(session);
|
||||
sample = provider.createActivitySample();
|
||||
|
||||
sample.setActiveCalories(ActivitySample.NOT_MEASURED);
|
||||
sample.setDevice(device);
|
||||
sample.setDistanceCm(ActivitySample.NOT_MEASURED);
|
||||
sample.setHeartRate(hr);
|
||||
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
|
||||
sample.setRawKind(ActivityKind.UNKNOWN.getCode());
|
||||
sample.setSteps(ActivitySample.NOT_MEASURED);
|
||||
sample.setTimestamp((int) (System.currentTimeMillis() / 1000));
|
||||
sample.setUser(user);
|
||||
|
||||
provider.addGBActivitySample(sample);
|
||||
} catch (Exception e) {
|
||||
GB.toast(getContext(), "Error saving activity samples", Toast.LENGTH_LONG, GB.ERROR, e);
|
||||
return;
|
||||
}
|
||||
|
||||
publish(sample);
|
||||
}
|
||||
}
|
||||
|
||||
private void publish(final GarminActivitySample sample) {
|
||||
final Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES);
|
||||
intent.setPackage(BuildConfig.APPLICATION_ID);
|
||||
intent.putExtra(GBDevice.EXTRA_DEVICE, getDevice());
|
||||
intent.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2129,6 +2129,7 @@
|
||||
<string name="devicetype_vivitar_hr_bp_monitor_activity_tracker" translatable="false">Vivitar HR & BP Monitor Activity Tracker</string>
|
||||
<string name="devicetype_hama_fit6900" translatable="false">Hama Fit6900</string>
|
||||
<string name="devicetype_generic_thermal_printer">Thermal Printer</string>
|
||||
<string name="devicetype_garmin_hrm_pro_plus" translatable="false">Garmin HRM-Pro Plus</string>
|
||||
<string name="devicetype_generic_headphones">Headphones</string>
|
||||
<string name="devicetype_generic_heart_rate_sensor">Heart Rate Sensor</string>
|
||||
<string name="devicetype_generic_weight_scale">Weight Scale</string>
|
||||
|
||||
+1
@@ -121,6 +121,7 @@ public class AbstractDeviceCoordinatorTest extends TestBase {
|
||||
put("CASIO STB-1000", DeviceType.CASIOGB6900); // #1902
|
||||
put("Amazfit Bip 3 Pro", DeviceType.AMAZFITBIP3PRO); // #3249
|
||||
put("Redmi Band Pro AB01", DeviceType.REDMISMARTBANDPRO); // #3069
|
||||
put("HRMPro+:123456", DeviceType.GARMIN_HRM_PRO_PLUS); // #5364
|
||||
put("Instinct 2S Solar", DeviceType.GARMIN_INSTINCT_2S_SOLAR); // #3063
|
||||
put("LE_WH-1000XM5", DeviceType.SONY_WH_1000XM5); // #2969
|
||||
put("WH-1000XM2", DeviceType.SONY_WH_1000XM2); // #2935
|
||||
|
||||
Reference in New Issue
Block a user