From dd46ba54be6642692412d140bc65cd12772859c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 18 Apr 2026 17:44:19 +0100 Subject: [PATCH] Polar H10: Add configuration settings --- .../DeviceSettingsPreferenceConst.java | 5 + .../DeviceSpecificSettingsFragment.java | 5 + .../polar/PolarH10DeviceCoordinator.java | 9 ++ .../service/btle/BleNamesResolver.java | 2 + .../devices/polar/PolarH10DeviceSupport.java | 121 ++++++++++++++++++ app/src/main/res/values/strings.xml | 8 ++ app/src/main/res/xml/devicesettings_polar.xml | 30 +++++ 7 files changed, 180 insertions(+) create mode 100644 app/src/main/res/xml/devicesettings_polar.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index 733434b4fd..96e9073bd0 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -575,6 +575,11 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_SONY_ADAPTIVE_VOLUME_CONTROL = "pref_adaptive_volume_control"; public static final String PREF_SONY_WIDE_AREA_TAP = "pref_wide_area_tap"; + public static final String PREF_GYMLINK_ENABLED = "gymlink_enabled"; + public static final String PREF_ANTPLUS_ENABLED = "antplus_enabled"; + public static final String PREF_HR_BROADCAST = "hr_broadcast"; + public static final String PREF_DUAL_CONNECTION = "dual_connection"; + public static final String PREF_MEDIA_SOURCE = "pref_media_source"; public static final String PREF_MEDIA_PLAYBACK_MODE = "pref_media_playback_mode"; public static final String PREF_SHOKZ_EQUALIZER_BLUETOOTH = "pref_shokz_equalizer_bluetooth"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 4073e56266..2ac30b8959 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -857,6 +857,11 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_SONY_ADAPTIVE_VOLUME_CONTROL); addPreferenceHandlerFor(PREF_SONY_WIDE_AREA_TAP); + addPreferenceHandlerFor(PREF_GYMLINK_ENABLED); + addPreferenceHandlerFor(PREF_ANTPLUS_ENABLED); + addPreferenceHandlerFor(PREF_HR_BROADCAST); + addPreferenceHandlerFor(PREF_DUAL_CONNECTION); + addPreferenceHandlerFor(PREF_MEDIA_SOURCE); addPreferenceHandlerFor(PREF_MEDIA_PLAYBACK_MODE); addPreferenceHandlerFor(PREF_SHOKZ_EQUALIZER_BLUETOOTH); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/polar/PolarH10DeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/polar/PolarH10DeviceCoordinator.java index c364a5679c..057b070dbb 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/polar/PolarH10DeviceCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/polar/PolarH10DeviceCoordinator.java @@ -3,6 +3,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.polar; import java.util.regex.Pattern; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; public class PolarH10DeviceCoordinator extends AbstractPolarDeviceCoordinator { @Override @@ -14,4 +16,11 @@ public class PolarH10DeviceCoordinator extends AbstractPolarDeviceCoordinator { protected Pattern getSupportedDeviceName() { return Pattern.compile("^Polar H10.*"); } + + @Override + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { + final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_polar); + return deviceSpecificSettings; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java index dec98a3b40..89e967a6a6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java @@ -597,6 +597,7 @@ public class BleNamesResolver { mServices.put("000056ff-0000-1000-8000-00805f9b34fb", "(Propr: GloryFit Data"); mServices.put("af9df7a1-e595-11e3-96b4-0002a5d5c51b", "(Propr: OneTouch"); mServices.put("9b012401-bc30-ce9a-e111-0f67e491abde", "(Propr: Garmin GFDI V0)"); + mServices.put("6217ff4b-fb31-1140-ad5a-a45545d7ecf3", "(Propr: Polar Settings)"); mServices.put("6a4e2401-667b-11e3-949a-0800200c9a66", "(Propr: Garmin GFDI V1)"); mServices.put("6a4e2800-667b-11e3-949a-0800200c9a66", "(Propr: Garmin ML)"); mServices.put("86f61000-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman State)"); @@ -1194,6 +1195,7 @@ public class BleNamesResolver { mCharacteristics.put("86f65002-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Response)"); mCharacteristics.put("86f66001-f706-58a0-95b2-1fb9261e4dc7", "(Propr: Ultrahuman Data)"); mCharacteristics.put("da2e7828-fbce-4e01-ae9e-261174997c48", "(Propr: SMP - Simple Management Protocol)"); + mCharacteristics.put("6217ff4d-91bb-91d0-7e2a-7cd3bda8a1f3", "(Propr: Polar Settings)"); mCharacteristics.put("6e400002-b5a3-f393-e0a9-e50e24dcca9e", "(Propr: Nordic UART TX)"); mCharacteristics.put("6e400003-b5a3-f393-e0a9-e50e24dcca9e", "(Propr: Nordic UART RX)"); mCharacteristics.put("de5bf729-d711-4e47-af26-65e3012a5dc7", "(Propr: Yawell Notify)"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/polar/PolarH10DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/polar/PolarH10DeviceSupport.java index 8de05742d2..4dec42c187 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/polar/PolarH10DeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/polar/PolarH10DeviceSupport.java @@ -1,5 +1,10 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.polar; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_ANTPLUS_ENABLED; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_DUAL_CONNECTION; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_GYMLINK_ENABLED; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_HR_BROADCAST; + import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; @@ -18,6 +23,7 @@ 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.deviceevents.GBDeviceEventUpdatePreferences; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.HeartRrIntervalSampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.polar.PolarH10ActivitySampleProvider; @@ -50,12 +56,26 @@ public class PolarH10DeviceSupport extends AbstractBTLESingleDeviceSupport { public static final UUID UUID_SERVICE_HEART_RATE = GattService.UUID_SERVICE_HEART_RATE; public static final UUID UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT = GattCharacteristic.UUID_CHARACTERISTIC_HEART_RATE_MEASUREMENT; + public static final UUID UUID_SERVICE_POLAR_SETTINGS = UUID.fromString("6217ff4b-fb31-1140-ad5a-a45545d7ecf3"); + public static final UUID UUID_CHARACTERISTIC_POLAR_SETTINGS = UUID.fromString("6217ff4d-91bb-91d0-7e2a-7cd3bda8a1f3"); + + public static final byte CMD_HR_BROADCAST_SET = (byte) 0x01; + public static final byte CMD_HR_BROADCAST_GET = (byte) 0x02; + public static final byte CMD_GYM_LINK_SET = (byte) 0x03; + public static final byte CMD_GYM_LINK_GET = (byte) 0x04; + public static final byte CMD_DUAL_CONNECTION_SET = (byte) 0x08; + public static final byte CMD_DUAL_CONNECTION_GET = (byte) 0x09; + public static final byte CMD_ANT_SET = (byte) 0x0a; + public static final byte CMD_ANT_GET = (byte) 0x0b; + public static final byte CMD_RESPONSE = (byte) 0xf0; + public PolarH10DeviceSupport() { super(LOG); addSupportedService(UUID_SERVICE_DEVICE_INFORMATION); addSupportedService(UUID_SERVICE_BATTERY_SERVICE); addSupportedService(UUID_SERVICE_HEART_RATE); + addSupportedService(UUID_SERVICE_POLAR_SETTINGS); IntentListener mListener = intent -> { String action = intent.getAction(); @@ -101,6 +121,12 @@ public class PolarH10DeviceSupport extends AbstractBTLESingleDeviceSupport { heartRateProfile.enableNotify(builder, true); + builder.notify(UUID_CHARACTERISTIC_POLAR_SETTINGS, true); + builder.write(UUID_CHARACTERISTIC_POLAR_SETTINGS, CMD_HR_BROADCAST_GET); + builder.write(UUID_CHARACTERISTIC_POLAR_SETTINGS, CMD_GYM_LINK_GET); + builder.write(UUID_CHARACTERISTIC_POLAR_SETTINGS, CMD_DUAL_CONNECTION_GET); + builder.write(UUID_CHARACTERISTIC_POLAR_SETTINGS, CMD_ANT_GET); + // Set defaults getDevice().setFirmwareVersion("N/A"); getDevice().setFirmwareVersion2("N/A"); @@ -142,11 +168,106 @@ public class PolarH10DeviceSupport extends AbstractBTLESingleDeviceSupport { return true; } + if (UUID_CHARACTERISTIC_POLAR_SETTINGS.equals(characteristic.getUuid())) { + handlePolarSetting(value); + return true; + } + LOG.warn("Unhandled characteristic change: {} = {}", characteristic.getUuid(), GB.hexdump(value)); return false; } + @Override + public void onSendConfiguration(final String config) { + final byte preferenceCommand; + final boolean preferenceValue; + + switch (config) { + case PREF_GYMLINK_ENABLED: + preferenceCommand = CMD_GYM_LINK_SET; + preferenceValue = getDevicePrefs().getBoolean(config, false); + break; + case PREF_ANTPLUS_ENABLED: + preferenceCommand = CMD_ANT_SET; + preferenceValue = getDevicePrefs().getBoolean(config, false); + break; + case PREF_HR_BROADCAST: + preferenceCommand = CMD_HR_BROADCAST_SET; + preferenceValue = getDevicePrefs().getBoolean(config, false); + break; + case PREF_DUAL_CONNECTION: + preferenceCommand = CMD_DUAL_CONNECTION_SET; + preferenceValue = getDevicePrefs().getBoolean(config, false); + break; + default: + super.onSendConfiguration(config); + return; + } + + LOG.debug("Setting {} = {}", config, preferenceValue); + + final TransactionBuilder builder = createTransactionBuilder("set " + config + " = " + preferenceValue); + builder.write(UUID_CHARACTERISTIC_POLAR_SETTINGS, preferenceCommand, (byte) (preferenceValue ? 1 : 0)); + builder.queue(); + } + + private void handlePolarSetting(final byte[] value) { + if (value[0] != CMD_RESPONSE) { + LOG.warn("Unexpected first byte {}", String.format("0x%02x", value[0])); + return; + } + + switch (value[1]) { + case CMD_HR_BROADCAST_SET: + LOG.debug("HR broadcast set response, success = {}", value[2]); + break; + case CMD_HR_BROADCAST_GET: + if (value[2] != 0x01) { + LOG.warn("HR broadcast get failed, status = {}", String.format("0x%02x", value[2])); + return; + } + LOG.debug("Got HR broadcast setting, value = {}", value[3] != 0); + evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_HR_BROADCAST, value[3] != 0)); + break; + case CMD_GYM_LINK_SET: + LOG.debug("GymLink set response, success = {}", value[2]); + break; + case CMD_GYM_LINK_GET: + if (value[2] != 0x01) { + LOG.warn("GymLink get failed, status = {}", String.format("0x%02x", value[2])); + return; + } + LOG.debug("Got GymLink setting, value = {}", value[3] != 0); + evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_GYMLINK_ENABLED, value[3] != 0)); + break; + case CMD_DUAL_CONNECTION_SET: + LOG.debug("Dual connection set response, success = {}", value[2]); + break; + case CMD_DUAL_CONNECTION_GET: + if (value[2] != 0x01) { + LOG.warn("Dual connection get failed, status = {}", String.format("0x%02x", value[2])); + return; + } + LOG.debug("Got dual connection setting, value = {}", value[3] != 0); + evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_DUAL_CONNECTION, value[3] != 0)); + break; + case CMD_ANT_SET: + LOG.debug("ANT+ set response, success = {}", value[2]); + break; + case CMD_ANT_GET: + if (value[2] != 0x01) { + LOG.warn("ANT+ get failed, status = {}", String.format("0x%02x", value[2])); + return; + } + LOG.debug("Got ANT+ setting, value = {}", value[3] != 0); + evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(PREF_ANTPLUS_ENABLED, value[3] != 0)); + break; + default: + LOG.warn("Unknown response byte: {}", String.format("0x%02x", value[1])); + } + } + private void handleDeviceInfo(nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.deviceinfo.DeviceInfo info) { LOG.warn("Device info: {}", info); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eccb32d42b..afb3e45f85 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4769,4 +4769,12 @@ Step goal progress Control music player Ring phone + GymLink + Allow GymLink-compatible equipment to connect to the device. + ANT+ + Nearby compatible ANT+ devices can pick up your heart rate. + Broadcast heart rate + Any nearby device or app can pick up your heart rate. + Dual connection + Allow two apps or devices to connect simultaneously. diff --git a/app/src/main/res/xml/devicesettings_polar.xml b/app/src/main/res/xml/devicesettings_polar.xml new file mode 100644 index 0000000000..eb2fb74f1f --- /dev/null +++ b/app/src/main/res/xml/devicesettings_polar.xml @@ -0,0 +1,30 @@ + + + + + + + + + +