Zepp OS: Add debug support for more config types

As seen when requesting all config arguments from the Amazfit GTR 4.
This commit is contained in:
José Rebelo
2026-01-01 20:55:10 +00:00
parent dc3ee7f4a0
commit a27d4f6de2
6 changed files with 180 additions and 11 deletions
@@ -35,6 +35,7 @@ import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.appmanager.AppManagerActivity;
@@ -549,6 +550,9 @@ public abstract class ZeppOsCoordinator extends HuamiCoordinator {
}
developer.add(R.xml.devicesettings_keep_activity_data_on_device);
developer.add(R.xml.devicesettings_huami2021_fetch_operation_time_unit);
if (BuildConfig.DEBUG) {
developer.add(R.xml.devicesettings_zeppos_request_all_configs);
}
return deviceSpecificSettings;
}
@@ -320,7 +320,20 @@ public class ZeppOsSupport extends AbstractDeviceSupport
@Override
public void onTestNewFunction() {
//final ZeppOsTransactionBuilder builder = createZeppOsTransactionBuilder("test new function");
//configService.requestConfig(
// builder,
// ZeppOsConfigService.ConfigGroup.AGPS,
// true,
// Collections.singletonList(ZeppOsConfigService.ConfigArg.AGPS_UNK_0x08)
//);
//configService.requestConfig(
// builder,
// ZeppOsConfigService.ConfigGroup.WORKOUT,
// true,
// Collections.singletonList(ZeppOsConfigService.ConfigArg.WORKOUT_HEART_RATE_ZONES)
//);
//builder.queue();
}
@Override
@@ -79,12 +79,14 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsS
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiLanguageType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.AbstractZeppOsService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.ZeppOsTransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigIntUnbound;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigBoolean;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigByte;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigByteList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigDatetimeHhMm;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigInt;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigShort;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigShortList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigString;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigStringList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config.ConfigTimestamp;
@@ -362,7 +364,13 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
}
public void requestConfig(final ZeppOsTransactionBuilder builder, final ConfigGroup config) {
requestConfig(builder, config, true, ZeppOsConfigService.ConfigArg.getAllArgsForConfigGroup(config));
if (BuildConfig.DEBUG && getSupport().getDevicePrefs().getBoolean("zepp_os_request_all_config_args", false)) {
LOG.debug("Requesting all config args for {}", config);
requestConfig(builder, config, true, Collections.emptyList());
} else {
// More conservative approach, since we may get config types we don't know how to parse
requestConfig(builder, config, true, ZeppOsConfigService.ConfigArg.getAllArgsForConfigGroup(config));
}
}
public void requestConfig(final ZeppOsTransactionBuilder builder,
@@ -429,13 +437,13 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
STRING(0x20),
STRING_LIST(0x21),
SHORT(0x01),
// TODO 0x02
SHORT_LIST(0x02),
INT(0x03),
BYTE(0x10),
BYTE_LIST(0x11),
DATETIME_HH_MM(0x30),
TIMESTAMP_MILLIS(0x40),
// TODO 0x50
INT_UNBOUND(0x50),
;
private final byte value;
@@ -461,6 +469,7 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
public enum ConfigArg {
// AGPS
AGPS_UNK_0x08(ConfigGroup.AGPS, ConfigType.INT_UNBOUND, 0x08, null), // TODO ?
AGPS_UPDATE_TIME(ConfigGroup.AGPS, ConfigType.TIMESTAMP_MILLIS, 0x09, PREF_AGPS_UPDATE_TIME),
AGPS_EXPIRE_TIME(ConfigGroup.AGPS, ConfigType.TIMESTAMP_MILLIS, 0x0a, PREF_AGPS_EXPIRE_TIME),
@@ -543,6 +552,7 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
WORKOUT_DETECTION_ALERT(ConfigGroup.WORKOUT, ConfigType.BOOL, 0x41, PREF_WORKOUT_DETECTION_ALERT),
WORKOUT_DETECTION_SENSITIVITY(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x42, PREF_WORKOUT_DETECTION_SENSITIVITY),
WORKOUT_POOL_SWIMMING_SIZE(ConfigGroup.WORKOUT, ConfigType.BYTE, 0x51, null), // TODO ?
WORKOUT_HEART_RATE_ZONES(ConfigGroup.WORKOUT, ConfigType.SHORT_LIST, 0x05, null),
// System
TIME_FORMAT(ConfigGroup.SYSTEM, ConfigType.BYTE, 0x01, PREF_TIMEFORMAT),
@@ -1069,6 +1079,13 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
argPrefs = convertShortToPrefs(configArg, valShort);
}
break;
case SHORT_LIST:
final ConfigShortList valShortList = ConfigShortList.consume(buf, includesConstraints);
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valShortList);
if (configArg != null) {
// TODO
}
break;
case INT:
final ConfigInt valInt = ConfigInt.consume(buf, includesConstraints);
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valInt);
@@ -1108,6 +1125,13 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
argPrefs = convertTimestampToPrefs(configArg, valTimestamp);
}
break;
case INT_UNBOUND:
final ConfigIntUnbound valIntUnbound = ConfigIntUnbound.consume(buf, includesConstraints);
LOG.info("Got {} ({}) = {}", configArg, String.format("0x%02x", configArgByte), valIntUnbound);
if (configArg != null) {
// TODO
}
break;
default:
LOG.error("No parser for {}", configArg);
// Abort, since we don't know how to parse this type or how many bytes it is
@@ -1391,13 +1415,15 @@ public class ZeppOsConfigService extends AbstractZeppOsService {
decoder = null;
}
if (decoder != null) {
prefs = singletonMap(configArg.getPrefKey(), decoder.decode(value.getValue()));
if (includesConstraints) {
prefs.put(
DeviceSettingsUtils.getPrefPossibleValuesKey(configArg.getPrefKey()),
TextUtils.join(",", decodeByteValues(possibleValues, decoder))
);
if (configArg.getPrefKey() != null) {
if (decoder != null) {
prefs = singletonMap(configArg.getPrefKey(), decoder.decode(value.getValue()));
if (includesConstraints) {
prefs.put(
DeviceSettingsUtils.getPrefPossibleValuesKey(configArg.getPrefKey()),
TextUtils.join(",", decodeByteValues(possibleValues, decoder))
);
}
}
}
@@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
import java.util.Locale;
@SuppressWarnings("ClassCanBeRecord")
public class ConfigIntUnbound {
private final int value;
public ConfigIntUnbound(final int value) {
this.value = value;
}
public int getValue() {
return value;
}
public static ConfigIntUnbound consume(final ByteBuffer buf, final boolean includesConstraints) {
final int value = buf.getInt();
// Looks to have no constraints at all
return new ConfigIntUnbound(value);
}
@NonNull
@Override
public String toString() {
return String.format(Locale.ROOT, "ConfigIntUnbound{value=%s}", String.format("0x%08x", value));
}
}
@@ -0,0 +1,83 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.services.config;
import androidx.annotation.NonNull;
import java.nio.ByteBuffer;
@SuppressWarnings("ClassCanBeRecord")
public class ConfigShortList {
private final short[] values;
private final int minCount;
private final int maxCount;
private final short minValue;
private final short maxValue;
public ConfigShortList(final short[] values,
final int minCount,
final int maxCount,
final short minValue,
final short maxValue) {
this.values = values;
this.minCount = minCount;
this.maxCount = maxCount;
this.minValue = minValue;
this.maxValue = maxValue;
}
public static ConfigShortList consume(final ByteBuffer buf, final boolean includesConstraints) {
final int numValues = buf.get() & 0xff;
final short[] values = new short[numValues];
for (int i = 0; i < numValues; i++) {
values[i] = buf.getShort();
}
if (!includesConstraints) {
return new ConfigShortList(
values,
0,
Integer.MAX_VALUE,
Short.MIN_VALUE,
Short.MAX_VALUE
);
}
// only seen in WORKOUT 0x05 in what looks like HR zones: [80,100,120,140,160,180]
final int minCount = buf.get() & 0xff; // 6
final int maxCount = buf.get() & 0xff; // 6
final short minValue = buf.getShort(); // 30
final short maxValue = buf.getShort(); // 220
return new ConfigShortList(
values,
minCount,
maxCount,
minValue,
maxValue
);
}
@NonNull
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("ConfigShortList{values=[");
for (short value : values) {
sb.append(value).append(",");
}
if (values.length > 0) {
sb.setLength(sb.length() - 1); // remove last ,
}
sb.append("]");
if (maxCount != Integer.MAX_VALUE) {
sb.append(", minCount=").append(minCount);
sb.append(", maxCount=").append(maxCount);
sb.append(", minValue=").append(minValue);
sb.append(", maxValue=").append(maxValue);
}
sb.append("}");
return sb.toString();
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_settings_applications"
android:key="zepp_os_request_all_config_args"
android:layout="@layout/preference_checkbox"
android:summary="Some may fail to be parsed - check the logs for errors."
android:title="Request all config args" />
</androidx.preference.PreferenceScreen>