mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
feat(withings,scanwatch): Brightness support, 5 alarms, quicklook support, light sleep
This commit is contained in:
+22
-4
@@ -84,6 +84,8 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
boolean smartAlarmSupported = supportsSmartWakeup(alarm.getPosition());
|
||||
boolean smartAlarmForced = forcedSmartWakeup(alarm.getPosition());
|
||||
boolean smartAlarmIntervalSupported = supportsSmartWakeupInterval(alarm.getPosition());
|
||||
final int smartWakeupMax = getSmartWakeupMaxInterval();
|
||||
final String smartWakeupDescription = getSmartWakeupDescription();
|
||||
|
||||
binding.cbSmartWakeup.setChecked(alarm.getSmartWakeup() || smartAlarmForced);
|
||||
binding.cbSmartWakeup.setVisibility(smartAlarmSupported ? View.VISIBLE : View.GONE);
|
||||
@@ -105,19 +107,20 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
if (alarm.getSmartWakeupInterval() != null) {
|
||||
binding.cbSmartWakeupInterval.setText(NumberFormat.getInstance().format(alarm.getSmartWakeupInterval()));
|
||||
}
|
||||
final int maxDigits = String.valueOf(smartWakeupMax).length();
|
||||
binding.cbSmartWakeupInterval.setFilters(new InputFilter[]{
|
||||
new InputFilter() {
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
|
||||
if (dend >= 3) // Limit length
|
||||
if (dend >= maxDigits + 1) // Limit length
|
||||
return "";
|
||||
|
||||
String strValue = dest.subSequence(0, dstart) + source.subSequence(start, end).toString() + dest.subSequence(dend, dest.length());
|
||||
try {
|
||||
int value = Integer.parseInt(strValue);
|
||||
if (value > 255) {
|
||||
binding.cbSmartWakeupInterval.setText("255");
|
||||
binding.cbSmartWakeupInterval.setSelection(3); // Move cursor to end
|
||||
if (value > smartWakeupMax) {
|
||||
binding.cbSmartWakeupInterval.setText(String.valueOf(smartWakeupMax));
|
||||
binding.cbSmartWakeupInterval.setSelection(String.valueOf(smartWakeupMax).length()); // Move cursor to end
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return "";
|
||||
@@ -127,6 +130,13 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
}
|
||||
});
|
||||
|
||||
if (smartAlarmIntervalSupported && smartWakeupDescription != null) {
|
||||
binding.tvSmartWakeupDescription.setText(smartWakeupDescription);
|
||||
binding.tvSmartWakeupDescription.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.tvSmartWakeupDescription.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
binding.cbSnooze.setChecked(alarm.getSnooze());
|
||||
binding.cbSnooze.setVisibility(supportsSnoozing() ? View.VISIBLE : View.GONE);
|
||||
|
||||
@@ -195,6 +205,14 @@ public class AlarmDetails extends AbstractGBActivity {
|
||||
return device.getDeviceCoordinator().supportsSmartWakeupInterval(device, position);
|
||||
}
|
||||
|
||||
private int getSmartWakeupMaxInterval() {
|
||||
return device.getDeviceCoordinator().getSmartWakeupMaxInterval(device);
|
||||
}
|
||||
|
||||
private String getSmartWakeupDescription() {
|
||||
return device.getDeviceCoordinator().getSmartWakeupDescription(device);
|
||||
}
|
||||
|
||||
/**
|
||||
* The alarm at this position *must* be a smart alarm
|
||||
*/
|
||||
|
||||
+10
@@ -546,6 +546,16 @@ public abstract class AbstractDeviceCoordinator implements DeviceCoordinator {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSmartWakeupMaxInterval(@NonNull GBDevice device) {
|
||||
return 255;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSmartWakeupDescription(@NonNull GBDevice device) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean forcedSmartWakeup(GBDevice device, int alarmPosition) {
|
||||
return false;
|
||||
|
||||
@@ -550,6 +550,19 @@ public interface DeviceCoordinator {
|
||||
*/
|
||||
boolean supportsSmartWakeupInterval(@NonNull GBDevice device, int alarmPosition);
|
||||
|
||||
/**
|
||||
* Returns the maximum smart wakeup interval in minutes supported by this device.
|
||||
* Defaults to 255. Override to restrict (e.g. Scanwatch supports 0-60).
|
||||
*/
|
||||
int getSmartWakeupMaxInterval(@NonNull GBDevice device);
|
||||
|
||||
/**
|
||||
* Returns an optional human-readable description explaining how smart wakeup works on this
|
||||
* device, or {@code null} if no description should be shown.
|
||||
*/
|
||||
@Nullable
|
||||
String getSmartWakeupDescription(@NonNull GBDevice device);
|
||||
|
||||
/**
|
||||
* Returns true if the alarm at the specified position *must* be a smart alarm for this device/coordinator
|
||||
* @param alarmPosition Position of the alarm
|
||||
|
||||
+18
-2
@@ -24,6 +24,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
@@ -51,7 +52,7 @@ public class WithingsScanwatchDeviceCoordinator extends AbstractBLEDeviceCoordin
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{ R.xml.devicesettings_withingsscanwatch };
|
||||
return new int[]{ R.xml.devicesettings_withingsscanwatch, R.xml.devicesettings_wearlocation };
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -91,7 +92,7 @@ public class WithingsScanwatchDeviceCoordinator extends AbstractBLEDeviceCoordin
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount(GBDevice gbDevice) {
|
||||
return 3;
|
||||
return 5;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -109,6 +110,21 @@ public class WithingsScanwatchDeviceCoordinator extends AbstractBLEDeviceCoordin
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeupInterval(@NonNull GBDevice device, int alarmPosition) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSmartWakeupMaxInterval(@NonNull GBDevice device) {
|
||||
return 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSmartWakeupDescription(@NonNull GBDevice device) {
|
||||
return GBApplication.getContext().getString(R.string.withings_scanwatch_alarm_smart_wakeup_description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
|
||||
+2
-11
@@ -58,21 +58,12 @@ public class WithingsScanwatchSampleProvider extends AbstractSampleProvider<With
|
||||
|
||||
@Override
|
||||
public ActivityKind normalizeType(int rawType) {
|
||||
switch (rawType) {
|
||||
case 1: return ActivityKind.LIGHT_SLEEP;
|
||||
case 2: return ActivityKind.DEEP_SLEEP;
|
||||
default: return ActivityKind.fromCode(rawType);
|
||||
}
|
||||
return ActivityKind.fromCode(rawType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(ActivityKind activityKind) {
|
||||
switch (activityKind) {
|
||||
case UNKNOWN: return 0;
|
||||
case LIGHT_SLEEP: return 1;
|
||||
case DEEP_SLEEP: return 2;
|
||||
default: return activityKind.getCode();
|
||||
}
|
||||
return activityKind.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+54
-5
@@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.devices.withingsscanwatch;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
@@ -30,26 +31,74 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class WithingsScanwatchSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
|
||||
static final String PREF_SCREENS_SORTABLE = "withings_scanwatch_screens_sortable";
|
||||
static final String PREF_SHORTCUT_ACTION = "withings_scanwatch_shortcut_action";
|
||||
static final String PREF_ECG_ENABLED = "withings_scanwatch_ecg_enabled";
|
||||
static final String PREF_AFIB_ENABLED = "withings_scanwatch_afib_enabled";
|
||||
static final String PREF_SCREENS_SORTABLE = "withings_scanwatch_screens_sortable";
|
||||
static final String PREF_SHORTCUT_ACTION = "withings_scanwatch_shortcut_action";
|
||||
static final String PREF_ECG_ENABLED = "withings_scanwatch_ecg_enabled";
|
||||
static final String PREF_RESPIRATORY_SCAN = "withings_scanwatch_respiratory_scan";
|
||||
static final String PREF_AFIB_DAY_ENABLED = "withings_scanwatch_afib_day_enabled";
|
||||
static final String PREF_AFIB_NIGHT_ENABLED= "withings_scanwatch_afib_night_enabled";
|
||||
static final String PREF_QUICKLOOK = "withings_scanwatch_quicklook";
|
||||
static final String PREF_AUTO_BRIGHTNESS = "withings_scanwatch_auto_brightness";
|
||||
static final String PREF_BRIGHTNESS_LEVEL = "withings_scanwatch_brightness_level";
|
||||
static final String PREF_MOVE_HANDS = "withings_scanwatch_move_hands";
|
||||
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) {
|
||||
handler.addPreferenceHandlerFor(PREF_SCREENS_SORTABLE);
|
||||
handler.addPreferenceHandlerFor(PREF_SHORTCUT_ACTION);
|
||||
handler.addPreferenceHandlerFor(PREF_ECG_ENABLED);
|
||||
handler.addPreferenceHandlerFor(PREF_AFIB_ENABLED);
|
||||
handler.addPreferenceHandlerFor(PREF_RESPIRATORY_SCAN);
|
||||
handler.addPreferenceHandlerFor(PREF_AFIB_DAY_ENABLED);
|
||||
handler.addPreferenceHandlerFor(PREF_AFIB_NIGHT_ENABLED);
|
||||
handler.addPreferenceHandlerFor(PREF_QUICKLOOK);
|
||||
handler.addPreferenceHandlerFor(PREF_AUTO_BRIGHTNESS);
|
||||
handler.addPreferenceHandlerFor(PREF_MOVE_HANDS);
|
||||
|
||||
// Brightness level is only meaningful when auto-brightness is OFF (inverse dependency)
|
||||
final EditTextPreference brightnessPref = handler.findPreference(PREF_BRIGHTNESS_LEVEL);
|
||||
if (brightnessPref != null) {
|
||||
final boolean autoOn = prefs.getBoolean(PREF_AUTO_BRIGHTNESS, true);
|
||||
brightnessPref.setEnabled(!autoOn);
|
||||
}
|
||||
|
||||
// Register brightness level handler with validation: integer only, clamped to 0-100
|
||||
handler.addPreferenceHandlerFor(PREF_BRIGHTNESS_LEVEL, (pref, newValue) -> {
|
||||
final String raw = String.valueOf(newValue).trim();
|
||||
try {
|
||||
final int val = Integer.parseInt(raw);
|
||||
final int clamped = Math.max(0, Math.min(100, val));
|
||||
if (clamped != val && pref instanceof EditTextPreference) {
|
||||
// Persist the clamped value instead of the out-of-range one
|
||||
((EditTextPreference) pref).setText(String.valueOf(clamped));
|
||||
return false; // reject original; clamped value already persisted
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
return false; // reject non-integer input
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
final ListPreference shortcutPref = handler.findPreference(PREF_SHORTCUT_ACTION);
|
||||
if (shortcutPref != null) {
|
||||
shortcutPref.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
|
||||
}
|
||||
|
||||
final ListPreference respiratoryPref = handler.findPreference(PREF_RESPIRATORY_SCAN);
|
||||
if (respiratoryPref != null) {
|
||||
respiratoryPref.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
|
||||
if (PREF_AUTO_BRIGHTNESS.equals(preference.getKey())) {
|
||||
final EditTextPreference brightnessPref = handler.findPreference(PREF_BRIGHTNESS_LEVEL);
|
||||
if (brightnessPref != null) {
|
||||
// preference value has already been persisted at this point
|
||||
final boolean autoOn = preference.getSharedPreferences().getBoolean(PREF_AUTO_BRIGHTNESS, true);
|
||||
brightnessPref.setEnabled(!autoOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+88
-15
@@ -23,6 +23,7 @@ import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -51,12 +52,17 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
|
||||
|
||||
private GBDevice device;
|
||||
private LocalBroadcastManager localBroadcastManager;
|
||||
private final String[] calibrationAdvices = new String[3];
|
||||
private final String[] appDialAdvices = new String[3];
|
||||
private final String[] crownAdvices = new String[3];
|
||||
private final Hands[] hands = new Hands[]{Hands.HOURS, Hands.MINUTES, Hands.ACTIVITY_TARGET};
|
||||
private short handIndex = 0;
|
||||
private Button previousButton;
|
||||
private Button nextButton;
|
||||
private Button okButton;
|
||||
private Button confirmButton;
|
||||
private RotaryControl rotaryControl;
|
||||
private TextView textView;
|
||||
private boolean crownMode = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -90,12 +96,18 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
appDialAdvices[0] = getString(R.string.withings_calibration_text_hours);
|
||||
appDialAdvices[1] = getString(R.string.withings_calibration_text_minutes);
|
||||
appDialAdvices[2] = getString(R.string.withings_calibration_text_activity_target);
|
||||
|
||||
calibrationAdvices[0] = getString(R.string.withings_calibration_text_hours);
|
||||
calibrationAdvices[1] = getString(R.string.withings_calibration_text_minutes);
|
||||
calibrationAdvices[2] = getString(R.string.withings_calibration_text_activity_target);
|
||||
crownAdvices[0] = getString(R.string.withings_calibration_crown_text_hours);
|
||||
crownAdvices[1] = getString(R.string.withings_calibration_crown_text_minutes);
|
||||
crownAdvices[2] = getString(R.string.withings_calibration_crown_text_activity_target);
|
||||
|
||||
textView = findViewById(R.id.withings_calibration_textview);
|
||||
rotaryControl = findViewById(R.id.rotary_control);
|
||||
confirmButton = findViewById(R.id.withings_calibration_button_confirm);
|
||||
|
||||
RotaryControl rotaryControl = findViewById(R.id.rotary_control);
|
||||
rotaryControl.setRotationListener(new RotaryControl.RotationListener() {
|
||||
@Override
|
||||
public void onRotation(short movementAmount) {
|
||||
@@ -106,26 +118,39 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
|
||||
}
|
||||
});
|
||||
|
||||
TextView textView = findViewById(R.id.withings_calibration_textview);
|
||||
textView.setText(calibrationAdvices[0]);
|
||||
// Crown mode: confirm button registers current position as 12 o'clock
|
||||
confirmButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent calibration = new Intent(WithingsBaseDeviceSupport.HANDS_CALIBRATION_CMD);
|
||||
calibration.putExtra("hand", hands[handIndex].code);
|
||||
calibration.putExtra("movementAmount", (short) 0);
|
||||
localBroadcastManager.sendBroadcast(calibration);
|
||||
// Auto-advance to next hand or finish
|
||||
if (handIndex < 2) {
|
||||
handIndex++;
|
||||
updateUI();
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
previousButton = findViewById(R.id.withings_calibration_button_previous);
|
||||
previousButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handIndex--;
|
||||
enableButtons();
|
||||
textView.setText(calibrationAdvices[handIndex]);
|
||||
rotaryControl.reset();
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
|
||||
nextButton = findViewById(R.id.withings_calibration_button_next);
|
||||
nextButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
handIndex++;
|
||||
enableButtons();
|
||||
textView.setText(calibrationAdvices[handIndex]);
|
||||
rotaryControl.reset();
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,7 +162,55 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
|
||||
}
|
||||
});
|
||||
|
||||
enableButtons();
|
||||
// Mode toggle
|
||||
RadioGroup modeGroup = findViewById(R.id.withings_calibration_mode_group);
|
||||
modeGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, int checkedId) {
|
||||
crownMode = (checkedId == R.id.withings_calibration_mode_crown);
|
||||
updateUI();
|
||||
}
|
||||
});
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
if (crownMode) {
|
||||
rotaryControl.setVisibility(View.GONE);
|
||||
confirmButton.setVisibility(View.VISIBLE);
|
||||
// Hide Previous/Next/OK - crown mode auto-advances on Confirm
|
||||
previousButton.setVisibility(View.GONE);
|
||||
nextButton.setVisibility(View.GONE);
|
||||
okButton.setVisibility(View.GONE);
|
||||
textView.setText(crownAdvices[handIndex]);
|
||||
// Send a tiny movement to tell the watch which hand the crown should
|
||||
// control. Without this the firmware doesn't know which hand is active
|
||||
// and the crown may move the wrong hand (or none at all).
|
||||
selectHandForCrown();
|
||||
} else {
|
||||
rotaryControl.setVisibility(View.VISIBLE);
|
||||
confirmButton.setVisibility(View.GONE);
|
||||
previousButton.setVisibility(View.VISIBLE);
|
||||
nextButton.setVisibility(View.VISIBLE);
|
||||
okButton.setVisibility(View.VISIBLE);
|
||||
textView.setText(appDialAdvices[handIndex]);
|
||||
rotaryControl.reset();
|
||||
enableButtons();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a minimal MOVE_HAND command to activate the current hand for crown
|
||||
* input. The watch firmware only routes crown rotation to a hand after it
|
||||
* has received at least one MOVE_HAND for that hand.
|
||||
*/
|
||||
private void selectHandForCrown() {
|
||||
if (localBroadcastManager == null) return;
|
||||
Intent calibration = new Intent(WithingsBaseDeviceSupport.HANDS_CALIBRATION_CMD);
|
||||
calibration.putExtra("hand", hands[handIndex].code);
|
||||
calibration.putExtra("movementAmount", (short) 1);
|
||||
localBroadcastManager.sendBroadcast(calibration);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -156,4 +229,4 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
|
||||
previousButton.setEnabled(handIndex > 0);
|
||||
okButton.setEnabled(handIndex == 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-18
@@ -58,28 +58,12 @@ public class WithingsSteelHRSampleProvider extends AbstractSampleProvider<Within
|
||||
|
||||
@Override
|
||||
public ActivityKind normalizeType(int rawType) {
|
||||
switch (rawType) {
|
||||
case 1:
|
||||
return ActivityKind.LIGHT_SLEEP;
|
||||
case 2:
|
||||
return ActivityKind.DEEP_SLEEP;
|
||||
default:
|
||||
return ActivityKind.fromCode(rawType);
|
||||
}
|
||||
return ActivityKind.fromCode(rawType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(ActivityKind activityKind) {
|
||||
switch (activityKind) {
|
||||
case UNKNOWN:
|
||||
return 0;
|
||||
case LIGHT_SLEEP:
|
||||
return 1;
|
||||
case DEEP_SLEEP:
|
||||
return 2;
|
||||
default:
|
||||
return activityKind.getCode();
|
||||
}
|
||||
return activityKind.getCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+227
-40
@@ -27,6 +27,7 @@ import java.util.Objects;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.withingsscanwatch.WithingsScanwatchSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.AbstractWithingsActivitySample;
|
||||
@@ -35,17 +36,34 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUIDs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.GetShortcutHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.GetGlanceHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.GetLuminosityHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.GetMoveHandsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.GetWearPosHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.EndOfTransmission;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.FeatureTagDeprecated;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.FeatureTagsUserId;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GlanceStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LocalNotification;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LuminosityLevel;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ScreenSettings;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ShortcutAction;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TrackerMoveHands;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TrackerWearPos;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsScreenId;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import androidx.annotation.NonNull;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.IconHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageMetaData;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreen;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.ExpectedResponse;
|
||||
|
||||
/**
|
||||
* Device support for Withings Scanwatch. The Scanwatch uses the same BLE protocol as the
|
||||
* Steel HR but with a different service UUID suffix (005d vs 0037).
|
||||
@@ -70,10 +88,22 @@ public class WithingsScanwatchDeviceSupport extends WithingsBaseDeviceSupport {
|
||||
|
||||
private static final String PREF_SHORTCUT_ACTION = "withings_scanwatch_shortcut_action";
|
||||
|
||||
/** Whether the ECG measurement feature is enabled on the watch (feature tag activation). */
|
||||
static final String PREF_ECG_ENABLED = "withings_scanwatch_ecg_enabled";
|
||||
/** Whether AFib detection (and night AFib) is enabled on the watch. */
|
||||
static final String PREF_AFIB_ENABLED = "withings_scanwatch_afib_enabled";
|
||||
/** Whether the ECG measurement feature is enabled on the watch. */
|
||||
static final String PREF_ECG_ENABLED = "withings_scanwatch_ecg_enabled";
|
||||
/** Respiratory scan mode: "off", "automatic" (some nights), or "always" (every night). */
|
||||
static final String PREF_RESPIRATORY_SCAN = "withings_scanwatch_respiratory_scan";
|
||||
/** Whether daytime AFib detection is enabled on the watch. */
|
||||
static final String PREF_AFIB_DAY_ENABLED = "withings_scanwatch_afib_day_enabled";
|
||||
/** Whether night-time AFib detection is enabled on the watch. */
|
||||
static final String PREF_AFIB_NIGHT_ENABLED = "withings_scanwatch_afib_night_enabled";
|
||||
/** Whether quicklook / glance (raise-to-wake) is enabled on the watch. */
|
||||
static final String PREF_QUICKLOOK = "withings_scanwatch_quicklook";
|
||||
/** Whether auto-brightness is enabled on the watch. */
|
||||
static final String PREF_AUTO_BRIGHTNESS = "withings_scanwatch_auto_brightness";
|
||||
/** Manual brightness level (0-100) when auto-brightness is off. */
|
||||
static final String PREF_BRIGHTNESS_LEVEL = "withings_scanwatch_brightness_level";
|
||||
/** Whether tracker move-hands (move hands to 10:10 when screen turns on) is enabled. */
|
||||
static final String PREF_MOVE_HANDS = "withings_scanwatch_move_hands";
|
||||
|
||||
@Override
|
||||
protected void addExtraSyncCommands() {
|
||||
@@ -81,6 +111,32 @@ public class WithingsScanwatchDeviceSupport extends WithingsBaseDeviceSupport {
|
||||
new WithingsMessage(WithingsMessageType.GET_SHORTCUT),
|
||||
new GetShortcutHandler(this)
|
||||
);
|
||||
addSimpleConversationToQueue(
|
||||
new WithingsMessage(WithingsMessageType.GLANCE_GET),
|
||||
new GetGlanceHandler(this)
|
||||
);
|
||||
addSimpleConversationToQueue(
|
||||
new WithingsMessage(WithingsMessageType.GET_LUMINOSITY_LEVEL),
|
||||
new GetLuminosityHandler(this)
|
||||
);
|
||||
addSimpleConversationToQueue(
|
||||
new WithingsMessage(WithingsMessageType.GET_TRACKER_MOVE_HANDS),
|
||||
new GetMoveHandsHandler(this)
|
||||
);
|
||||
addSimpleConversationToQueue(
|
||||
new WithingsMessage(WithingsMessageType.GET_TRACKER_WEAR_POS),
|
||||
new GetWearPosHandler(this)
|
||||
);
|
||||
|
||||
// Re-push feature tags and local notifications on every sync so the watch
|
||||
// retains the correct state after a Bluetooth reconnect or reboot.
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
final boolean ecg = prefs.getBoolean(PREF_ECG_ENABLED, false);
|
||||
final String respScan = prefs.getString(PREF_RESPIRATORY_SCAN, "off");
|
||||
final boolean afibDay = prefs.getBoolean(PREF_AFIB_DAY_ENABLED, false);
|
||||
final boolean afibNight = prefs.getBoolean(PREF_AFIB_NIGHT_ENABLED, false);
|
||||
addFeatureTagsCommand(ecg, respScan, afibDay, afibNight);
|
||||
addLocalNotificationsCommand(afibDay, afibNight);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -108,21 +164,51 @@ public class WithingsScanwatchDeviceSupport extends WithingsBaseDeviceSupport {
|
||||
sendQueue();
|
||||
return true;
|
||||
}
|
||||
if (PREF_ECG_ENABLED.equals(config)) {
|
||||
if (PREF_ECG_ENABLED.equals(config) || PREF_RESPIRATORY_SCAN.equals(config)
|
||||
|| PREF_AFIB_DAY_ENABLED.equals(config) || PREF_AFIB_NIGHT_ENABLED.equals(config)) {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
final boolean enabled = prefs.getBoolean(PREF_ECG_ENABLED, false);
|
||||
final boolean ecg = prefs.getBoolean(PREF_ECG_ENABLED, false);
|
||||
final String respScan = prefs.getString(PREF_RESPIRATORY_SCAN, "off");
|
||||
final boolean afibDay = prefs.getBoolean(PREF_AFIB_DAY_ENABLED, false);
|
||||
final boolean afibNight = prefs.getBoolean(PREF_AFIB_NIGHT_ENABLED, false);
|
||||
clearQueue();
|
||||
addFeatureTagsCommand(enabled, prefs.getBoolean(PREF_AFIB_ENABLED, false));
|
||||
addLocalNotificationsCommand(enabled, prefs.getBoolean(PREF_AFIB_ENABLED, false));
|
||||
addFeatureTagsCommand(ecg, respScan, afibDay, afibNight);
|
||||
addLocalNotificationsCommand(afibDay, afibNight);
|
||||
sendQueue();
|
||||
return true;
|
||||
}
|
||||
if (PREF_AFIB_ENABLED.equals(config)) {
|
||||
if (PREF_QUICKLOOK.equals(config)) {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
final boolean afibEnabled = prefs.getBoolean(PREF_AFIB_ENABLED, false);
|
||||
final boolean enabled = prefs.getBoolean(PREF_QUICKLOOK, false);
|
||||
final WithingsMessage msg = new WithingsMessage(WithingsMessageType.GLANCE_SET,
|
||||
new GlanceStatus(enabled));
|
||||
clearQueue();
|
||||
addFeatureTagsCommand(prefs.getBoolean(PREF_ECG_ENABLED, false), afibEnabled);
|
||||
addLocalNotificationsCommand(prefs.getBoolean(PREF_ECG_ENABLED, false), afibEnabled);
|
||||
addSimpleConversationToQueue(msg);
|
||||
sendQueue();
|
||||
return true;
|
||||
}
|
||||
if (PREF_AUTO_BRIGHTNESS.equals(config) || PREF_BRIGHTNESS_LEVEL.equals(config)) {
|
||||
sendLuminosityLevel();
|
||||
return true;
|
||||
}
|
||||
if (PREF_MOVE_HANDS.equals(config)) {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
final boolean enabled = prefs.getBoolean(PREF_MOVE_HANDS, false);
|
||||
final WithingsMessage msg = new WithingsMessage(WithingsMessageType.SET_TRACKER_MOVE_HANDS,
|
||||
new TrackerMoveHands(enabled));
|
||||
clearQueue();
|
||||
addSimpleConversationToQueue(msg);
|
||||
sendQueue();
|
||||
return true;
|
||||
}
|
||||
if (DeviceSettingsPreferenceConst.PREF_WEARLOCATION.equals(config)) {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
final String location = prefs.getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left");
|
||||
final byte pos = "left".equals(location) ? TrackerWearPos.POS_LEFT_WRIST : TrackerWearPos.POS_RIGHT_WRIST;
|
||||
final WithingsMessage msg = new WithingsMessage(WithingsMessageType.SET_TRACKER_WEAR_POS,
|
||||
new TrackerWearPos(pos));
|
||||
clearQueue();
|
||||
addSimpleConversationToQueue(msg);
|
||||
sendQueue();
|
||||
return true;
|
||||
}
|
||||
@@ -130,31 +216,81 @@ public class WithingsScanwatchDeviceSupport extends WithingsBaseDeviceSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a {@code CMD_FEATURE_TAGS_SET_DEPRECATED_V2} (0x0987) message activating the
|
||||
* feature tags required for ECG and/or AFib.
|
||||
*
|
||||
* <p>The full tag set observed from HCI captures:
|
||||
* <ul>
|
||||
* <li>ECG enabled: tags 0x0004, 0x000F, 0x0035, 0x0058</li>
|
||||
* <li>AFib enabled (in addition to ECG tags): 0x000A, 0x000B</li>
|
||||
* </ul>
|
||||
* All tags use startTime=0 / endTime=0 (always active).
|
||||
*
|
||||
* <p>Note: the Withings app also sends 0x0035 and 0x0058 (purpose unknown) as a baseline.
|
||||
* Reads the current auto-brightness / brightness-level preferences and sends the
|
||||
* corresponding {@code CMD_SET_LUMINOSITY_LEVEL} (0x0941) command to the watch.
|
||||
*/
|
||||
private void addFeatureTagsCommand(final boolean ecgEnabled, final boolean afibEnabled) {
|
||||
private void sendLuminosityLevel() {
|
||||
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
|
||||
final boolean autoMode = prefs.getBoolean(PREF_AUTO_BRIGHTNESS, true);
|
||||
final byte mode;
|
||||
final byte level;
|
||||
if (autoMode) {
|
||||
mode = LuminosityLevel.MODE_AUTO;
|
||||
level = 0;
|
||||
} else {
|
||||
mode = LuminosityLevel.MODE_MANUAL;
|
||||
int rawLevel;
|
||||
try {
|
||||
rawLevel = Integer.parseInt(prefs.getString(PREF_BRIGHTNESS_LEVEL, "50"));
|
||||
} catch (NumberFormatException e) {
|
||||
rawLevel = 50;
|
||||
}
|
||||
level = (byte) Math.max(0, Math.min(100, rawLevel));
|
||||
}
|
||||
final WithingsMessage msg = new WithingsMessage(WithingsMessageType.SET_LUMINOSITY_LEVEL,
|
||||
new LuminosityLevel(mode, level));
|
||||
clearQueue();
|
||||
addSimpleConversationToQueue(msg);
|
||||
sendQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a {@code CMD_FEATURE_TAGS_SET_DEPRECATED_V2} (0x0987) message activating the
|
||||
* feature tags required for ECG, respiratory scan, and/or AFib.
|
||||
*
|
||||
* <p>Tag set from HCI captures:
|
||||
* <ul>
|
||||
* <li>ECG on: tags 0x0004, 0x000F, 0x0035, 0x0058</li>
|
||||
* <li>Respiratory automatic: tag 0x0005 + 0x000F, 0x0035, 0x0058</li>
|
||||
* <li>Respiratory always: tags 0x000E, 0x0011 + 0x000F, 0x0035, 0x0058</li>
|
||||
* <li>AFib daytime: tags 0x0009, 0x000A</li>
|
||||
* <li>AFib night: tag 0x000B</li>
|
||||
* </ul>
|
||||
* 0x0035 and 0x0058 are sent whenever any health feature is active (purpose unknown).
|
||||
* 0x000F (TAG_ECG_MEAS) is shared between ECG and respiratory scan; sent once even if both on.
|
||||
*/
|
||||
private void addFeatureTagsCommand(final boolean ecgEnabled, final String respiratoryScan,
|
||||
final boolean afibDayEnabled, final boolean afibNightEnabled) {
|
||||
final WithingsMessage msg = new WithingsMessage(WithingsMessageType.SET_FEATURE_TAGS_DEPRECATED);
|
||||
msg.addDataStructure(new FeatureTagsUserId()); // userId = 0
|
||||
|
||||
final boolean respAutomatic = "automatic".equals(respiratoryScan);
|
||||
final boolean respAlways = "always".equals(respiratoryScan);
|
||||
final boolean respActive = respAutomatic || respAlways;
|
||||
|
||||
if (ecgEnabled) {
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_ECG_TERMS));
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_ECG_MEAS));
|
||||
}
|
||||
if (afibEnabled) {
|
||||
if (respAutomatic) {
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_SPO2_SLEEP));
|
||||
}
|
||||
if (respAlways) {
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_RESP_ALWAYS));
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_0x0011));
|
||||
}
|
||||
if (afibDayEnabled) {
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_AFIB_WINDOW));
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_AFIB_EXTRA));
|
||||
}
|
||||
if (afibNightEnabled) {
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_AFIB_NIGHT));
|
||||
}
|
||||
if (ecgEnabled || afibEnabled) {
|
||||
if (ecgEnabled || respActive) {
|
||||
// TAG_ECG_MEAS (0x000F) enables ECG/respiratory measurements; send once even if both on
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_ECG_MEAS));
|
||||
}
|
||||
if (ecgEnabled || respActive || afibDayEnabled || afibNightEnabled) {
|
||||
// Baseline tags always present when any health feature is active
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_0x0035));
|
||||
msg.addDataStructure(new FeatureTagDeprecated(FeatureTagDeprecated.TAG_0x0058));
|
||||
}
|
||||
@@ -168,29 +304,80 @@ public class WithingsScanwatchDeviceSupport extends WithingsBaseDeviceSupport {
|
||||
*
|
||||
* <p>All five slots must always be sent together. Slot mapping from HCI captures:
|
||||
* <ol>
|
||||
* <li>PPG_AFIB - enabled when {@code afibEnabled}</li>
|
||||
* <li>ECG - always disabled (managed via feature tags)</li>
|
||||
* <li>UNKNOWN_3 - always disabled</li>
|
||||
* <li>HIGH_LOW_HR - enabled when {@code ecgEnabled} (observed in ECG capture)</li>
|
||||
* <li>PPG_AFIB_NIGHT - enabled when {@code afibEnabled}</li>
|
||||
* <li>PPG_AFIB - enabled when AFib daytime is on</li>
|
||||
* <li>HIGH_HR - enabled when AFib daytime is on (high HR alert, tied to AFib/ECG state)</li>
|
||||
* <li>LOW_HR - enabled when AFib daytime is on (low HR alert, tied to AFib/ECG state)</li>
|
||||
* <li>SLOT_4 - purpose unknown; always disabled</li>
|
||||
* <li>PPG_AFIB_NIGHT - enabled when AFib night is on</li>
|
||||
* </ol>
|
||||
*/
|
||||
private void addLocalNotificationsCommand(final boolean ecgEnabled, final boolean afibEnabled) {
|
||||
private void addLocalNotificationsCommand(final boolean afibDayEnabled,
|
||||
final boolean afibNightEnabled) {
|
||||
final WithingsMessage msg = new WithingsMessage(WithingsMessageType.SET_LOCAL_NOTIFICATIONS);
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_PPG_AFIB,
|
||||
afibEnabled ? LocalNotification.STATUS_ENABLED : LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_ECG,
|
||||
afibDayEnabled ? LocalNotification.STATUS_ENABLED : LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_HIGH_HR,
|
||||
afibDayEnabled ? LocalNotification.STATUS_ENABLED : LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_LOW_HR,
|
||||
afibDayEnabled ? LocalNotification.STATUS_ENABLED : LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_SLOT_4,
|
||||
LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_UNKNOWN_3,
|
||||
LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_HIGH_LOW_HR,
|
||||
ecgEnabled ? LocalNotification.STATUS_ENABLED : LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new LocalNotification(LocalNotification.NOTIF_PPG_AFIB_NIGHT,
|
||||
afibEnabled ? LocalNotification.STATUS_ENABLED : LocalNotification.STATUS_DISABLED));
|
||||
afibNightEnabled ? LocalNotification.STATUS_ENABLED : LocalNotification.STATUS_DISABLED));
|
||||
msg.addDataStructure(new EndOfTransmission());
|
||||
addSimpleConversationToQueue(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a SET_WORKOUT_SCREEN message for the ScanWatch with correct mode/flags and
|
||||
* two icon sizes (20x20 at idx=0, 28x28 at idx=1) as required by the ScanWatch protocol.
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
protected Message createWorkoutScreenMessage(String workoutType) {
|
||||
final WithingsActivityType activityType = WithingsActivityType.fromPrefValue(workoutType);
|
||||
final int code = activityType.getCode();
|
||||
|
||||
Message message = new WithingsMessage(WithingsMessageType.SET_WORKOUT_SCREEN, ExpectedResponse.NONE);
|
||||
|
||||
// Workout screen settings with correct mode and flags
|
||||
WorkoutScreen workoutScreen = new WorkoutScreen();
|
||||
workoutScreen.setId(code);
|
||||
final int stringId = getContext().getResources().getIdentifier(
|
||||
"activity_type_" + workoutType, "string", getContext().getPackageName());
|
||||
workoutScreen.setName(getContext().getString(stringId));
|
||||
workoutScreen.setMode(activityType.getWorkoutMode());
|
||||
workoutScreen.yetunknown2 = activityType.getWorkoutFlags();
|
||||
message.addDataStructure(workoutScreen);
|
||||
|
||||
// Get the drawable for this activity type
|
||||
final int drawableId = activityType.toActivityKind().getIcon();
|
||||
final Drawable drawable = getContext().getDrawable(drawableId);
|
||||
|
||||
// Icon 0: 20x20
|
||||
ImageMetaData meta0 = new ImageMetaData();
|
||||
meta0.setWidth((byte) 20);
|
||||
meta0.setHeight((byte) 20);
|
||||
message.addDataStructure(meta0);
|
||||
|
||||
ImageData data0 = new ImageData();
|
||||
data0.setImageData(IconHelper.getIconBytesFromDrawable(drawable, 20, 20));
|
||||
message.addDataStructure(data0);
|
||||
|
||||
// Icon 1: 28x28
|
||||
ImageMetaData meta1 = new ImageMetaData();
|
||||
meta1.setIndex((byte) 1);
|
||||
meta1.setWidth((byte) 28);
|
||||
meta1.setHeight((byte) 28);
|
||||
message.addDataStructure(meta1);
|
||||
|
||||
ImageData data1 = new ImageData();
|
||||
data1.setImageData(IconHelper.getIconBytesFromDrawable(drawable, 28, 28));
|
||||
message.addDataStructure(data1);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the Scanwatch screen list from the user's drag-sort preference.
|
||||
*
|
||||
|
||||
+5
-2
@@ -26,9 +26,12 @@ import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
|
||||
public class IconHelper {
|
||||
|
||||
public static byte[] getIconBytesFromDrawable(Drawable drawable) {
|
||||
return getIconBytesFromDrawable(drawable, 22, 24);
|
||||
}
|
||||
|
||||
public static byte[] getIconBytesFromDrawable(Drawable drawable, int width, int height) {
|
||||
Bitmap bitmap = BitmapUtil.toBitmap(drawable);
|
||||
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 22, 24, true);
|
||||
int size = scaledBitmap.getRowBytes() * scaledBitmap.getHeight();
|
||||
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, width, height, true);
|
||||
return toByteArray(scaledBitmap);
|
||||
}
|
||||
|
||||
|
||||
+57
-21
@@ -53,6 +53,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
|
||||
@@ -65,6 +66,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.comm
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.BatteryStateHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.Conversation;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ConversationQueue;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.GetAlarmHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.HeartRateHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ResponseHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SetupFinishedHandler;
|
||||
@@ -246,7 +248,7 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time()));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.DISTANCE, getUnit())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.CLOCK_MODE, getTimeMode())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(activityUser.getStepsGoal())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(ActivityTarget.GOAL_TYPE_STEPS, activityUser.getStepsGoal())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true)));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
|
||||
@@ -288,12 +290,12 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
|
||||
// This makes the "authentication" far easier. However if it turns out that this is needed, we would need to find a way to savely store a unique generated secret.
|
||||
// message.addDataStructure(new UserSecret());
|
||||
addSimpleConversationToQueue(message);
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(activityUser.getStepsGoal())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET, new ActivityTarget(ActivityTarget.GOAL_TYPE_STEPS, activityUser.getStepsGoal())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.DISTANCE, getUnit())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_USER_UNIT, new UserUnit(UserUnitConstants.CLOCK_MODE, getTimeMode())));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM_SETTINGS));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_SCREEN_SETTINGS), new ScreenSettingsHandler(this));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_MULTI_ALARM), new GetAlarmHandler(this));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM_ENABLED));
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_WORKOUT_SCREEN_LIST), new WorkoutScreenListHandler(this));
|
||||
addExtraSyncCommands();
|
||||
@@ -372,25 +374,32 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
if (alarms.size() > 3) {
|
||||
throw new IllegalArgumentException("This device only has three alarm slots!");
|
||||
}
|
||||
|
||||
if (alarms.size() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
boolean noAlarmsEnabled = true;
|
||||
conversationQueue.clear();
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM));
|
||||
|
||||
// Build a single CMD_SET_MULTI_ALARM message containing ALL enabled alarms.
|
||||
// The watch replaces its entire alarm list with the contents of this message,
|
||||
// so sending one message per alarm would cause only the last one to survive.
|
||||
Message multiAlarmMessage = new WithingsMessage(WithingsMessageType.SET_ALARM);
|
||||
boolean anyAlarmEnabled = false;
|
||||
|
||||
for (Alarm alarm : alarms) {
|
||||
if (alarm.getEnabled() && !alarm.getUnused()) {
|
||||
noAlarmsEnabled = false;
|
||||
addAlarm(alarm);
|
||||
anyAlarmEnabled = true;
|
||||
addAlarmToMessage(multiAlarmMessage, alarm);
|
||||
}
|
||||
}
|
||||
|
||||
if (noAlarmsEnabled) {
|
||||
if (anyAlarmEnabled) {
|
||||
addSimpleConversationToQueue(multiAlarmMessage);
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(true)));
|
||||
} else {
|
||||
// Send an empty SET_ALARM to clear all alarms, then disable
|
||||
addSimpleConversationToQueue(multiAlarmMessage);
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(false)));
|
||||
}
|
||||
|
||||
@@ -437,6 +446,9 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
|
||||
case PREF_LANGUAGE:
|
||||
setLanguage();
|
||||
break;
|
||||
case ActivityUser.PREF_USER_STEPS_GOAL:
|
||||
sendStepsGoal();
|
||||
break;
|
||||
default:
|
||||
if (!handleExtraConfiguration(config)) {
|
||||
logger.debug("unknown configuration setting received: " + config);
|
||||
@@ -447,6 +459,14 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
|
||||
}
|
||||
}
|
||||
|
||||
private void sendStepsGoal() {
|
||||
ActivityUser user = new ActivityUser();
|
||||
conversationQueue.clear();
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ACTIVITY_TARGET,
|
||||
new ActivityTarget(ActivityTarget.GOAL_TYPE_STEPS, user.getStepsGoal())));
|
||||
conversationQueue.send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from {@link #onSendConfiguration(String)} for device-specific configuration keys
|
||||
* not handled by the base class.
|
||||
@@ -545,24 +565,40 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
|
||||
}
|
||||
}
|
||||
|
||||
private void addAlarm(Alarm alarm) {
|
||||
private void addAlarmToMessage(Message multiAlarmMessage, Alarm alarm) {
|
||||
AlarmSettings alarmSettings = new AlarmSettings();
|
||||
alarmSettings.setHour((short) alarm.getHour());
|
||||
alarmSettings.setMinute((short) alarm.getMinute());
|
||||
alarmSettings.setDayOfWeek(mapRepetitionToWithingsValue(alarm));
|
||||
if (alarm.getSmartWakeup()) {
|
||||
// Healthmate has the possibility to change the minutecount, in GB we use a fixed value of 15
|
||||
alarmSettings.setSmartWakeupMinutes((short) 15);
|
||||
|
||||
if (!alarm.isRepetitive()) {
|
||||
// One-time alarm: set the target date so the watch knows when to fire.
|
||||
// Without a date, flags=0x80 + date=00/00/00 is ambiguous and the watch
|
||||
// may ignore the alarm entirely.
|
||||
Calendar cal = AlarmUtils.toCalendar(alarm);
|
||||
alarmSettings.setDayOfMonth((short) cal.get(Calendar.DAY_OF_MONTH));
|
||||
alarmSettings.setMonth((short) (cal.get(Calendar.MONTH) + 1)); // Calendar.MONTH is 0-based
|
||||
alarmSettings.setYear((short) (cal.get(Calendar.YEAR) - 2000));
|
||||
}
|
||||
|
||||
Message alarmMessage = new WithingsMessage(WithingsMessageType.SET_ALARM, alarmSettings);
|
||||
if (alarm.getSmartWakeup()) {
|
||||
final Integer interval = alarm.getSmartWakeupInterval();
|
||||
final int maxInterval = gbDevice.getDeviceCoordinator().getSmartWakeupMaxInterval(gbDevice);
|
||||
short minutes;
|
||||
if (interval != null && interval > 0) {
|
||||
minutes = (short) Math.min(interval, maxInterval);
|
||||
} else {
|
||||
// Fall back to a sensible default (20 min) if not configured
|
||||
minutes = 20;
|
||||
}
|
||||
alarmSettings.setSmartWakeupMinutes(minutes);
|
||||
}
|
||||
|
||||
multiAlarmMessage.addDataStructure(alarmSettings);
|
||||
if (!StringUtils.isEmpty(alarm.getTitle())) {
|
||||
AlarmName alarmName = new AlarmName(alarm.getTitle());
|
||||
alarmMessage.addDataStructure(alarmName);
|
||||
multiAlarmMessage.addDataStructure(alarmName);
|
||||
}
|
||||
|
||||
addSimpleConversationToQueue(alarmMessage);
|
||||
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(true)));
|
||||
}
|
||||
|
||||
private short mapRepetitionToWithingsValue(Alarm alarm) {
|
||||
@@ -732,7 +768,7 @@ public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDevice
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Message createWorkoutScreenMessage(String workoutType) {
|
||||
protected Message createWorkoutScreenMessage(String workoutType) {
|
||||
WithingsActivityType withingsActivityType = WithingsActivityType.fromPrefValue(workoutType);
|
||||
int code = withingsActivityType.getCode();
|
||||
Message message = new WithingsMessage(WithingsMessageType.SET_WORKOUT_SCREEN, ExpectedResponse.NONE);
|
||||
|
||||
+49
@@ -163,6 +163,55 @@ public enum WithingsActivityType {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the workout mode byte used in the WORKOUT_SCREEN_SETTINGS TLV.
|
||||
* <ul>
|
||||
* <li>1 = Indoor / pool (no GPS, indoor-specific UI)</li>
|
||||
* <li>2 = GPS tracking (outdoor with GPS)</li>
|
||||
* <li>3 = No GPS (outdoor/generic without GPS)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public byte getWorkoutMode() {
|
||||
switch (this) {
|
||||
// GPS-tracked outdoor sports
|
||||
case WALKING:
|
||||
case RUNNING:
|
||||
case HIKING:
|
||||
case SURFING:
|
||||
case KITESURFING:
|
||||
case WINDSURFING:
|
||||
case SKIING:
|
||||
case SNOWBOARDING:
|
||||
case RIDING:
|
||||
return 2; // GPS
|
||||
// Indoor / pool sports
|
||||
case SWIMMING:
|
||||
case CLIMBING:
|
||||
case ELLIPTICAL:
|
||||
case WEIGHTLIFTING:
|
||||
case GYMNASTICS:
|
||||
case PILATES:
|
||||
case YOGA:
|
||||
case DANCING:
|
||||
case ZUMBA:
|
||||
case BOXING:
|
||||
case ICESKATING:
|
||||
case ROWING:
|
||||
return 1; // Indoor
|
||||
// Everything else: no GPS, not specifically indoor
|
||||
default:
|
||||
return 3; // NoGPS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the flags short for the WORKOUT_SCREEN_SETTINGS TLV.
|
||||
* Indoor/pool types use 0x0001; all others use 0x0000.
|
||||
*/
|
||||
public short getWorkoutFlags() {
|
||||
return getWorkoutMode() == 1 ? (short) 0x0001 : (short) 0x0000;
|
||||
}
|
||||
|
||||
public static WithingsActivityType fromPrefValue(final String prefValue) {
|
||||
for (final WithingsActivityType type : values()) {
|
||||
if (type.name().toLowerCase(Locale.ROOT).equals(prefValue.replace("_", "").toLowerCase(Locale.ROOT))) {
|
||||
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.conversation;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmName;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AlarmSettings;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
|
||||
|
||||
/**
|
||||
* Handles the response to {@code CMD_GET_MULTI_ALARM} (0x0146 / 326).
|
||||
*
|
||||
* <p>Reads alarm TLVs from the watch response, matches them by position with the
|
||||
* Gadgetbridge alarm database, and updates the DB so the UI reflects the watch's
|
||||
* current state (enabled/disabled, time, repetition, smart wakeup, title).
|
||||
*
|
||||
* <p>This enables two important behaviors:
|
||||
* <ul>
|
||||
* <li>One-time alarms auto-disable after firing (the watch clears the enabled bit)</li>
|
||||
* <li>Alarms toggled on the watch are reflected in Gadgetbridge on next sync</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class GetAlarmHandler extends AbstractResponseHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GetAlarmHandler.class);
|
||||
|
||||
public GetAlarmHandler(WithingsBaseDeviceSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(Message response) {
|
||||
List<WithingsStructure> data = response.getDataStructures();
|
||||
if (data == null) {
|
||||
logger.info("GetAlarmHandler: null data structures in response");
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect alarm settings and names from the response.
|
||||
// Each AlarmSettings may be followed by an AlarmName for the same slot.
|
||||
List<AlarmSettings> watchAlarms = new ArrayList<>();
|
||||
List<String> watchAlarmNames = new ArrayList<>();
|
||||
|
||||
for (WithingsStructure structure : data) {
|
||||
if (structure instanceof AlarmSettings) {
|
||||
watchAlarms.add((AlarmSettings) structure);
|
||||
// Placeholder name until we see an AlarmName TLV
|
||||
watchAlarmNames.add(null);
|
||||
} else if (structure instanceof AlarmName) {
|
||||
// The AlarmName belongs to the most recently seen AlarmSettings
|
||||
if (!watchAlarmNames.isEmpty()) {
|
||||
watchAlarmNames.set(watchAlarmNames.size() - 1, ((AlarmName) structure).getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("GetAlarmHandler: watch reported {} alarm(s)", watchAlarms.size());
|
||||
|
||||
// Get the current alarms from the GB database (ordered by position)
|
||||
List<Alarm> dbAlarms = DBHelper.getAlarms(device);
|
||||
|
||||
boolean changed = false;
|
||||
|
||||
for (int i = 0; i < dbAlarms.size(); i++) {
|
||||
Alarm dbAlarm = dbAlarms.get(i);
|
||||
|
||||
if (i < watchAlarms.size()) {
|
||||
AlarmSettings watchAlarm = watchAlarms.get(i);
|
||||
String name = watchAlarmNames.get(i);
|
||||
boolean watchEnabled = watchAlarm.isEnabled();
|
||||
int watchRepetition = watchAlarm.getRepetitionMask();
|
||||
int watchHour = watchAlarm.getHour();
|
||||
int watchMinute = watchAlarm.getMinute();
|
||||
int watchSmartWake = watchAlarm.getSmartWakeupMinutes();
|
||||
|
||||
// Update enabled state
|
||||
if (dbAlarm.getEnabled() != watchEnabled) {
|
||||
logger.info("GetAlarmHandler: alarm {} enabled {} -> {}",
|
||||
i, dbAlarm.getEnabled(), watchEnabled);
|
||||
dbAlarm.setEnabled(watchEnabled);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Update time
|
||||
if (dbAlarm.getHour() != watchHour || dbAlarm.getMinute() != watchMinute) {
|
||||
logger.info("GetAlarmHandler: alarm {} time {}:{} -> {}:{}",
|
||||
i, dbAlarm.getHour(), dbAlarm.getMinute(), watchHour, watchMinute);
|
||||
dbAlarm.setHour(watchHour);
|
||||
dbAlarm.setMinute(watchMinute);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Update repetition
|
||||
if (dbAlarm.getRepetition() != watchRepetition) {
|
||||
logger.info("GetAlarmHandler: alarm {} repetition {} -> {}",
|
||||
i, dbAlarm.getRepetition(), watchRepetition);
|
||||
dbAlarm.setRepetition(watchRepetition);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// Update smart wakeup
|
||||
if (watchSmartWake > 0) {
|
||||
if (!dbAlarm.getSmartWakeup() || !Integer.valueOf(watchSmartWake).equals(dbAlarm.getSmartWakeupInterval())) {
|
||||
dbAlarm.setSmartWakeup(true);
|
||||
dbAlarm.setSmartWakeupInterval(watchSmartWake);
|
||||
changed = true;
|
||||
}
|
||||
} else {
|
||||
if (dbAlarm.getSmartWakeup()) {
|
||||
dbAlarm.setSmartWakeup(false);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update title
|
||||
if (name != null && !name.equals(dbAlarm.getTitle())) {
|
||||
dbAlarm.setTitle(name);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
DBHelper.store(dbAlarm);
|
||||
}
|
||||
} else {
|
||||
// More DB alarms than watch alarms - disable any extras
|
||||
if (dbAlarm.getEnabled()) {
|
||||
logger.info("GetAlarmHandler: alarm {} not on watch, disabling", i);
|
||||
dbAlarm.setEnabled(false);
|
||||
DBHelper.store(dbAlarm);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
// Broadcast to refresh the alarm UI
|
||||
Intent intent = new Intent(DeviceService.ACTION_SAVE_ALARMS);
|
||||
LocalBroadcastManager.getInstance(GBApplication.getContext()).sendBroadcast(intent);
|
||||
logger.info("GetAlarmHandler: alarm database updated, UI refresh broadcast sent");
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.conversation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GlanceStatus;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
|
||||
|
||||
/**
|
||||
* Handles the response to {@code CMD_GLANCE_GET} (0x097B).
|
||||
*
|
||||
* <p>Reads the current quicklook/glance status from the watch and persists it to the
|
||||
* device-specific {@link SharedPreferences} under {@link #PREF_QUICKLOOK}.
|
||||
*/
|
||||
public class GetGlanceHandler extends AbstractResponseHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GetGlanceHandler.class);
|
||||
|
||||
/** Device-specific shared-prefs key for the quicklook/glance status (boolean). */
|
||||
public static final String PREF_QUICKLOOK = "withings_scanwatch_quicklook";
|
||||
|
||||
public GetGlanceHandler(WithingsBaseDeviceSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(Message response) {
|
||||
List<WithingsStructure> data = response.getDataStructures();
|
||||
if (data == null || data.isEmpty()) {
|
||||
logger.warn("GetGlanceHandler: received empty response");
|
||||
return;
|
||||
}
|
||||
for (WithingsStructure structure : data) {
|
||||
if (structure instanceof GlanceStatus) {
|
||||
final boolean enabled = ((GlanceStatus) structure).isEnabled();
|
||||
logger.info("GetGlanceHandler: quicklook/glance = {}", enabled);
|
||||
final SharedPreferences prefs =
|
||||
GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
prefs.edit()
|
||||
.putBoolean(PREF_QUICKLOOK, enabled)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.conversation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LuminosityLevel;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
|
||||
|
||||
/**
|
||||
* Handles the response to {@code CMD_GET_LUMINOSITY_LEVEL} (0x0942).
|
||||
*
|
||||
* <p>Reads the current brightness mode and level from the watch and persists them to
|
||||
* the device-specific {@link SharedPreferences}.
|
||||
*/
|
||||
public class GetLuminosityHandler extends AbstractResponseHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GetLuminosityHandler.class);
|
||||
|
||||
public static final String PREF_AUTO_BRIGHTNESS = "withings_scanwatch_auto_brightness";
|
||||
public static final String PREF_BRIGHTNESS_LEVEL = "withings_scanwatch_brightness_level";
|
||||
|
||||
public GetLuminosityHandler(WithingsBaseDeviceSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(Message response) {
|
||||
List<WithingsStructure> data = response.getDataStructures();
|
||||
if (data == null || data.isEmpty()) {
|
||||
logger.warn("GetLuminosityHandler: received empty response");
|
||||
return;
|
||||
}
|
||||
for (WithingsStructure structure : data) {
|
||||
if (structure instanceof LuminosityLevel) {
|
||||
final LuminosityLevel lum = (LuminosityLevel) structure;
|
||||
final boolean autoMode = lum.isAutoMode();
|
||||
final int level = lum.getLevel() & 0xFF;
|
||||
logger.info("GetLuminosityHandler: auto={}, level={}", autoMode, level);
|
||||
final SharedPreferences prefs =
|
||||
GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
prefs.edit()
|
||||
.putBoolean(PREF_AUTO_BRIGHTNESS, autoMode)
|
||||
.putString(PREF_BRIGHTNESS_LEVEL, String.valueOf(level))
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.conversation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TrackerMoveHands;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
|
||||
|
||||
/**
|
||||
* Handles the response to {@code CMD_TRACKER_MOVE_HANDS_GET} (0x09AC).
|
||||
*
|
||||
* <p>Reads the current tracker move-hands status from the watch and persists it to the
|
||||
* device-specific {@link SharedPreferences} under {@link #PREF_MOVE_HANDS}.
|
||||
*/
|
||||
public class GetMoveHandsHandler extends AbstractResponseHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GetMoveHandsHandler.class);
|
||||
|
||||
/** Device-specific shared-prefs key for the move-hands status (boolean). */
|
||||
public static final String PREF_MOVE_HANDS = "withings_scanwatch_move_hands";
|
||||
|
||||
public GetMoveHandsHandler(WithingsBaseDeviceSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(Message response) {
|
||||
List<WithingsStructure> data = response.getDataStructures();
|
||||
if (data == null || data.isEmpty()) {
|
||||
logger.warn("GetMoveHandsHandler: received empty response");
|
||||
return;
|
||||
}
|
||||
for (WithingsStructure structure : data) {
|
||||
if (structure instanceof TrackerMoveHands) {
|
||||
final boolean enabled = ((TrackerMoveHands) structure).isEnabled();
|
||||
logger.info("GetMoveHandsHandler: move-hands = {}", enabled);
|
||||
final SharedPreferences prefs =
|
||||
GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
prefs.edit()
|
||||
.putBoolean(PREF_MOVE_HANDS, enabled)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.conversation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TrackerWearPos;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
|
||||
|
||||
/**
|
||||
* Handles the response to {@code CMD_GET_TRACKER_WEAR_POS} (0x0150).
|
||||
*
|
||||
* <p>Reads the current wear position from the watch and persists it to the
|
||||
* device-specific {@link SharedPreferences} under
|
||||
* {@link DeviceSettingsPreferenceConst#PREF_WEARLOCATION} using the standard
|
||||
* {@code "left"} / {@code "right"} string values.
|
||||
*/
|
||||
public class GetWearPosHandler extends AbstractResponseHandler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GetWearPosHandler.class);
|
||||
|
||||
public GetWearPosHandler(WithingsBaseDeviceSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResponse(Message response) {
|
||||
List<WithingsStructure> data = response.getDataStructures();
|
||||
if (data == null || data.isEmpty()) {
|
||||
logger.warn("GetWearPosHandler: received empty response");
|
||||
return;
|
||||
}
|
||||
for (WithingsStructure structure : data) {
|
||||
if (structure instanceof TrackerWearPos) {
|
||||
final TrackerWearPos wearPos = (TrackerWearPos) structure;
|
||||
final String location = wearPos.isLeftWrist() ? "left" : "right";
|
||||
logger.info("GetWearPosHandler: wear position = {} (raw={})", location, wearPos.getPosition());
|
||||
final SharedPreferences prefs =
|
||||
GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
|
||||
prefs.edit()
|
||||
.putString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, location)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-8
@@ -20,18 +20,24 @@ import java.nio.ByteBuffer;
|
||||
|
||||
public class ActivityTarget extends WithingsStructure {
|
||||
|
||||
private long targetCount;
|
||||
public static final int GOAL_TYPE_STEPS = 0;
|
||||
public static final int GOAL_TYPE_SLEEP = 1;
|
||||
public static final int GOAL_TYPE_SWIM = 2;
|
||||
|
||||
public ActivityTarget(long targetCount) {
|
||||
this.targetCount = targetCount;
|
||||
private int goalType;
|
||||
private int value;
|
||||
|
||||
public ActivityTarget(int goalType, int value) {
|
||||
this.goalType = goalType;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public long getTargetCount() {
|
||||
return targetCount;
|
||||
public int getGoalType() {
|
||||
return goalType;
|
||||
}
|
||||
|
||||
public void setTargetCount(long targetCount) {
|
||||
this.targetCount = targetCount;
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -41,7 +47,8 @@ public class ActivityTarget extends WithingsStructure {
|
||||
|
||||
@Override
|
||||
protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
|
||||
rawDataBuffer.putLong(targetCount);
|
||||
rawDataBuffer.putInt(goalType);
|
||||
rawDataBuffer.putInt(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
@@ -23,10 +23,17 @@ public class AlarmName extends WithingsStructure {
|
||||
|
||||
private String name;
|
||||
|
||||
/** No-arg constructor required by {@link DataStructureFactory}. */
|
||||
public AlarmName() {}
|
||||
|
||||
public AlarmName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getLength() {
|
||||
return (short) ((name != null ? name.getBytes().length : 0) + 1 + HEADER_SIZE);
|
||||
@@ -37,6 +44,13 @@ public class AlarmName extends WithingsStructure {
|
||||
addStringAsBytesWithLengthByte(rawDataBuffer, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
|
||||
if (rawDataBuffer.remaining() >= 1) {
|
||||
name = getNextString(rawDataBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getType() {
|
||||
return WithingsStructureType.ALARM_NAME;
|
||||
|
||||
+46
@@ -27,6 +27,39 @@ public class AlarmSettings extends WithingsStructure {
|
||||
private short year;
|
||||
private short smartWakeupMinutes;
|
||||
|
||||
/** No-arg constructor required by {@link DataStructureFactory}. */
|
||||
public AlarmSettings() {}
|
||||
|
||||
/**
|
||||
* Returns whether this alarm is enabled (bit 7 of dayOfWeek flags).
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return (dayOfWeek & 0x80) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the repetition bitmask in Gadgetbridge format (MON=1..SUN=64),
|
||||
* extracted from the Withings flags byte (bits 0-6).
|
||||
* Returns 0 (ALARM_ONCE) if no day bits are set (one-time alarm).
|
||||
*/
|
||||
public int getRepetitionMask() {
|
||||
int gbRepetition = 0;
|
||||
// Withings: bit0=Sun, bit1=Mon, bit2=Tue, bit3=Wed, bit4=Thu, bit5=Fri, bit6=Sat
|
||||
// GB: SUN=64, MON=1, TUE=2, WED=4, THU=8, FRI=16, SAT=32
|
||||
if ((dayOfWeek & 0x01) != 0) gbRepetition |= 64; // Sun
|
||||
if ((dayOfWeek & 0x02) != 0) gbRepetition |= 1; // Mon
|
||||
if ((dayOfWeek & 0x04) != 0) gbRepetition |= 2; // Tue
|
||||
if ((dayOfWeek & 0x08) != 0) gbRepetition |= 4; // Wed
|
||||
if ((dayOfWeek & 0x10) != 0) gbRepetition |= 8; // Thu
|
||||
if ((dayOfWeek & 0x20) != 0) gbRepetition |= 16; // Fri
|
||||
if ((dayOfWeek & 0x40) != 0) gbRepetition |= 32; // Sat
|
||||
return gbRepetition;
|
||||
}
|
||||
|
||||
public short getSmartWakeupMinutes() {
|
||||
return smartWakeupMinutes;
|
||||
}
|
||||
|
||||
public short getHour() {
|
||||
return hour;
|
||||
}
|
||||
@@ -104,6 +137,19 @@ public class AlarmSettings extends WithingsStructure {
|
||||
return WithingsStructureType.ALARM;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
|
||||
if (rawDataBuffer.remaining() >= 7) {
|
||||
hour = (short) (rawDataBuffer.get() & 0xFF);
|
||||
minute = (short) (rawDataBuffer.get() & 0xFF);
|
||||
dayOfWeek = (short) (rawDataBuffer.get() & 0xFF);
|
||||
dayOfMonth = (short) (rawDataBuffer.get() & 0xFF);
|
||||
month = (short) (rawDataBuffer.get() & 0xFF);
|
||||
year = (short) (rawDataBuffer.get() & 0xFF);
|
||||
smartWakeupMinutes = (short) (rawDataBuffer.get() & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean withEndOfMessage() {
|
||||
return true;
|
||||
|
||||
+18
@@ -140,6 +140,24 @@ public class DataStructureFactory {
|
||||
case WithingsStructureType.LOCAL_NOTIFICATION:
|
||||
structure = new LocalNotification();
|
||||
break;
|
||||
case WithingsStructureType.GLANCE_STATUS:
|
||||
structure = new GlanceStatus();
|
||||
break;
|
||||
case WithingsStructureType.ALARM:
|
||||
structure = new AlarmSettings();
|
||||
break;
|
||||
case WithingsStructureType.ALARM_NAME:
|
||||
structure = new AlarmName();
|
||||
break;
|
||||
case WithingsStructureType.LUMINOSITY_LEVEL:
|
||||
structure = new LuminosityLevel();
|
||||
break;
|
||||
case WithingsStructureType.TRACKER_MOVE_HANDS:
|
||||
structure = new TrackerMoveHands();
|
||||
break;
|
||||
case WithingsStructureType.TRACKER_WEAR_POS:
|
||||
structure = new TrackerWearPos();
|
||||
break;
|
||||
default:
|
||||
structure = null;
|
||||
logger.info("Received yet unknown structure type: " + structureTypeFromResponse);
|
||||
|
||||
+22
-13
@@ -30,16 +30,19 @@ import java.nio.ByteBuffer;
|
||||
*
|
||||
* <p>Known feature tag IDs observed from HCI captures:
|
||||
* <ul>
|
||||
* <li>{@link #TAG_ECG_TERMS} (0x0004) - ECG terms & conditions accepted</li>
|
||||
* <li>{@link #TAG_AFIB_WINDOW} (0x0009) - AFib detection time window (uses timestamps)</li>
|
||||
* <li>{@link #TAG_AFIB_EXTRA} (0x000A) - AFib detection (always active)</li>
|
||||
* <li>{@link #TAG_AFIB_NIGHT} (0x000B) - Night-time AFib detection</li>
|
||||
* <li>{@link #TAG_ECG_MEAS} (0x000F) - ECG measurement enabled</li>
|
||||
* <li>{@link #TAG_IRREGULAR_HR}(0x0013) - Irregular heart rate detection</li>
|
||||
* <li>{@link #TAG_HIGH_HR} (0x0014) - High heart rate notification</li>
|
||||
* <li>{@link #TAG_LOW_HR} (0x0016) - Low heart rate notification</li>
|
||||
* <li>{@link #TAG_0x0035} (0x0035) - Present during ECG activation (purpose unknown)</li>
|
||||
* <li>{@link #TAG_0x0058} (0x0058) - Present during ECG activation (purpose unknown)</li>
|
||||
* <li>{@link #TAG_ECG_TERMS} (0x0004) - ECG terms & conditions accepted</li>
|
||||
* <li>{@link #TAG_SPO2_SLEEP} (0x0005) - Respiratory scan automatic/sleep mode</li>
|
||||
* <li>{@link #TAG_AFIB_WINDOW} (0x0009) - AFib detection time window (uses timestamps)</li>
|
||||
* <li>{@link #TAG_AFIB_EXTRA} (0x000A) - AFib detection (always active)</li>
|
||||
* <li>{@link #TAG_AFIB_NIGHT} (0x000B) - Night-time AFib detection</li>
|
||||
* <li>{@link #TAG_RESP_ALWAYS} (0x000E) - Respiratory scan always-on mode</li>
|
||||
* <li>{@link #TAG_ECG_MEAS} (0x000F) - ECG/respiratory measurement enabled</li>
|
||||
* <li>{@link #TAG_0x0011} (0x0011) - Companion to TAG_RESP_ALWAYS in always-on mode</li>
|
||||
* <li>{@link #TAG_IRREGULAR_HR} (0x0013) - Irregular heart rate detection</li>
|
||||
* <li>{@link #TAG_HIGH_HR} (0x0014) - High heart rate notification</li>
|
||||
* <li>{@link #TAG_LOW_HR} (0x0016) - Low heart rate notification</li>
|
||||
* <li>{@link #TAG_0x0035} (0x0035) - Present when any health feature is active (purpose unknown)</li>
|
||||
* <li>{@link #TAG_0x0058} (0x0058) - Present when any health feature is active (purpose unknown)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class FeatureTagDeprecated extends WithingsStructure {
|
||||
@@ -48,23 +51,29 @@ public class FeatureTagDeprecated extends WithingsStructure {
|
||||
|
||||
/** ECG terms & conditions accepted by the user. */
|
||||
public static final short TAG_ECG_TERMS = 0x0004;
|
||||
/** Respiratory scan automatic mode (scans some nights). */
|
||||
public static final short TAG_SPO2_SLEEP = 0x0005;
|
||||
/** AFib detection time window (populated with actual timestamps). */
|
||||
public static final short TAG_AFIB_WINDOW = 0x0009;
|
||||
/** AFib continuous detection (always-active, timestamps = 0). */
|
||||
public static final short TAG_AFIB_EXTRA = (short) 0x000A;
|
||||
/** Night-time AFib detection. */
|
||||
public static final short TAG_AFIB_NIGHT = (short) 0x000B;
|
||||
/** ECG measurement feature enabled. */
|
||||
/** Respiratory scan always-on mode (scans every night). */
|
||||
public static final short TAG_RESP_ALWAYS = (short) 0x000E;
|
||||
/** ECG / respiratory measurement feature enabled. */
|
||||
public static final short TAG_ECG_MEAS = (short) 0x000F;
|
||||
/** Companion to TAG_RESP_ALWAYS in always-on respiratory scan mode. */
|
||||
public static final short TAG_0x0011 = (short) 0x0011;
|
||||
/** Irregular heart rate detection. */
|
||||
public static final short TAG_IRREGULAR_HR = (short) 0x0013;
|
||||
/** High heart rate notification. */
|
||||
public static final short TAG_HIGH_HR = (short) 0x0014;
|
||||
/** Low heart rate notification. */
|
||||
public static final short TAG_LOW_HR = (short) 0x0016;
|
||||
/** Unknown; observed during ECG activation (always-active). */
|
||||
/** Unknown; present when any health feature is active. */
|
||||
public static final short TAG_0x0035 = 0x0035;
|
||||
/** Unknown; observed during ECG activation (always-active). */
|
||||
/** Unknown; present when any health feature is active. */
|
||||
public static final short TAG_0x0058 = 0x0058;
|
||||
|
||||
// ---- Payload ----
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.datastructures;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Quicklook / glance status (raise-to-wake).
|
||||
*
|
||||
* <p>TLV type {@code 0x097A} (2426), 1-byte payload:
|
||||
* <ul>
|
||||
* <li>{@code 0} = disabled</li>
|
||||
* <li>{@code 1} = enabled</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class GlanceStatus extends WithingsStructure {
|
||||
|
||||
private boolean enabled;
|
||||
|
||||
/** No-arg constructor required by {@link DataStructureFactory}. */
|
||||
public GlanceStatus() {}
|
||||
|
||||
public GlanceStatus(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getLength() {
|
||||
return 5; // 4 header + 1 data
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillinTypeSpecificData(ByteBuffer buffer) {
|
||||
buffer.put(enabled ? (byte) 1 : (byte) 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
|
||||
if (rawDataBuffer.remaining() >= 1) {
|
||||
enabled = rawDataBuffer.get() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getType() {
|
||||
return WithingsStructureType.GLANCE_STATUS;
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -20,10 +20,18 @@ import java.nio.ByteBuffer;
|
||||
|
||||
public class ImageMetaData extends WithingsStructure {
|
||||
|
||||
private byte unknown = 0x00;
|
||||
private byte index = 0x00;
|
||||
private byte width;
|
||||
private byte height;
|
||||
|
||||
public byte getIndex() {
|
||||
return index;
|
||||
}
|
||||
|
||||
public void setIndex(byte index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public byte getWidth() {
|
||||
return width;
|
||||
}
|
||||
@@ -47,14 +55,14 @@ public class ImageMetaData extends WithingsStructure {
|
||||
|
||||
@Override
|
||||
protected void fillinTypeSpecificData(ByteBuffer buffer) {
|
||||
buffer.put(unknown);
|
||||
buffer.put(index);
|
||||
buffer.put(width);
|
||||
buffer.put(height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
|
||||
unknown = rawDataBuffer.get();
|
||||
index = rawDataBuffer.get();
|
||||
width = rawDataBuffer.get();
|
||||
height = rawDataBuffer.get();
|
||||
}
|
||||
|
||||
+11
-11
@@ -31,10 +31,10 @@ import java.nio.ByteBuffer;
|
||||
*
|
||||
* <p>Known notification slot IDs observed from HCI captures:
|
||||
* <ul>
|
||||
* <li>{@link #NOTIF_PPG_AFIB} (1) - AFib detection alert</li>
|
||||
* <li>{@link #NOTIF_ECG} (2) - ECG-related notification (slot seen as disabled)</li>
|
||||
* <li>{@link #NOTIF_UNKNOWN_3} (3) - Unknown (always seen as disabled)</li>
|
||||
* <li>{@link #NOTIF_HIGH_LOW_HR} (4) - High/low heart rate alert (enabled during ECG setup)</li>
|
||||
* <li>{@link #NOTIF_PPG_AFIB} (1) - AFib daytime detection alert</li>
|
||||
* <li>{@link #NOTIF_HIGH_HR} (2) - High heart rate alert</li>
|
||||
* <li>{@link #NOTIF_LOW_HR} (3) - Low heart rate alert</li>
|
||||
* <li>{@link #NOTIF_SLOT_4} (4) - Purpose unknown (seen disabled unless ECG active)</li>
|
||||
* <li>{@link #NOTIF_PPG_AFIB_NIGHT} (5) - Night-time AFib detection alert</li>
|
||||
* </ul>
|
||||
*
|
||||
@@ -45,14 +45,14 @@ public class LocalNotification extends WithingsStructure {
|
||||
|
||||
// ---- Notification slot identifiers ----
|
||||
|
||||
/** AFib detection alert. */
|
||||
/** AFib daytime detection alert. */
|
||||
public static final byte NOTIF_PPG_AFIB = 1;
|
||||
/** ECG notification slot. */
|
||||
public static final byte NOTIF_ECG = 2;
|
||||
/** Unknown notification slot (appears always disabled). */
|
||||
public static final byte NOTIF_UNKNOWN_3 = 3;
|
||||
/** High/low heart-rate alert. */
|
||||
public static final byte NOTIF_HIGH_LOW_HR = 4;
|
||||
/** High heart rate alert. */
|
||||
public static final byte NOTIF_HIGH_HR = 2;
|
||||
/** Low heart rate alert. */
|
||||
public static final byte NOTIF_LOW_HR = 3;
|
||||
/** Purpose unknown (observed disabled unless ECG active). */
|
||||
public static final byte NOTIF_SLOT_4 = 4;
|
||||
/** Night-time AFib detection alert. */
|
||||
public static final byte NOTIF_PPG_AFIB_NIGHT = 5;
|
||||
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.datastructures;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Screen luminosity mode and level.
|
||||
*
|
||||
* <p>TLV type {@code 0x0937} (2359), 2-byte payload:
|
||||
* <ul>
|
||||
* <li>byte 0: mode - {@code 0} = auto, {@code 1} = manual</li>
|
||||
* <li>byte 1: level - {@code 0}-{@code 100} (only meaningful in manual mode)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class LuminosityLevel extends WithingsStructure {
|
||||
|
||||
public static final byte MODE_AUTO = 0;
|
||||
public static final byte MODE_MANUAL = 1;
|
||||
|
||||
private byte mode;
|
||||
private byte level;
|
||||
|
||||
/** No-arg constructor required by {@link DataStructureFactory}. */
|
||||
public LuminosityLevel() {}
|
||||
|
||||
public LuminosityLevel(byte mode, byte level) {
|
||||
this.mode = mode;
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public byte getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public byte getLevel() {
|
||||
return level;
|
||||
}
|
||||
|
||||
public boolean isAutoMode() {
|
||||
return mode == MODE_AUTO;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getLength() {
|
||||
return 6; // 4 header + 2 data
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillinTypeSpecificData(ByteBuffer buffer) {
|
||||
buffer.put(mode);
|
||||
buffer.put(level);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
|
||||
if (rawDataBuffer.remaining() >= 2) {
|
||||
mode = rawDataBuffer.get();
|
||||
level = rawDataBuffer.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getType() {
|
||||
return WithingsStructureType.LUMINOSITY_LEVEL;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ public class ScreenSettings extends WithingsStructure {
|
||||
|
||||
private int id;
|
||||
|
||||
private int userId = 123456;
|
||||
private int userId = 0;
|
||||
private int yetUnknown1 = 0;
|
||||
private int yetUnknown2 = 0;
|
||||
private byte idOnDevice;
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.datastructures;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Tracker move-hands status (move hands to 10:10 when the screen turns on).
|
||||
*
|
||||
* <p>TLV type {@code 0x09BB} (2491), 1-byte payload:
|
||||
* <ul>
|
||||
* <li>{@code 0} = disabled</li>
|
||||
* <li>{@code 1} = enabled</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class TrackerMoveHands extends WithingsStructure {
|
||||
|
||||
private boolean enabled;
|
||||
|
||||
/** No-arg constructor required by {@link DataStructureFactory}. */
|
||||
public TrackerMoveHands() {}
|
||||
|
||||
public TrackerMoveHands(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getLength() {
|
||||
return 5; // 4 header + 1 data
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillinTypeSpecificData(ByteBuffer buffer) {
|
||||
buffer.put(enabled ? (byte) 1 : (byte) 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
|
||||
if (rawDataBuffer.remaining() >= 1) {
|
||||
enabled = rawDataBuffer.get() != 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getType() {
|
||||
return WithingsStructureType.TRACKER_MOVE_HANDS;
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
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.withingssteelhr.communication.datastructures;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Tracker wear position (which wrist the watch is worn on).
|
||||
*
|
||||
* <p>TLV type {@code 0x012F} (303), 1-byte payload:
|
||||
* <ul>
|
||||
* <li>{@code 0} = not set</li>
|
||||
* <li>{@code 1} = hip</li>
|
||||
* <li>{@code 2} = left wrist</li>
|
||||
* <li>{@code 3} = right wrist</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class TrackerWearPos extends WithingsStructure {
|
||||
|
||||
public static final byte POS_LEFT_WRIST = 2;
|
||||
public static final byte POS_RIGHT_WRIST = 3;
|
||||
|
||||
private byte position;
|
||||
|
||||
/** No-arg constructor required by {@link DataStructureFactory}. */
|
||||
public TrackerWearPos() {}
|
||||
|
||||
public TrackerWearPos(byte position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public byte getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public void setPosition(byte position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the position is left wrist.
|
||||
*/
|
||||
public boolean isLeftWrist() {
|
||||
return position == POS_LEFT_WRIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getLength() {
|
||||
return 5; // 4 header + 1 data
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillinTypeSpecificData(ByteBuffer buffer) {
|
||||
buffer.put(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
|
||||
if (rawDataBuffer.remaining() >= 1) {
|
||||
position = rawDataBuffer.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getType() {
|
||||
return WithingsStructureType.TRACKER_WEAR_POS;
|
||||
}
|
||||
}
|
||||
+12
@@ -80,5 +80,17 @@ public class WithingsStructureType {
|
||||
/** Local (on-watch) notification slot configuration (type 0x09A8 = 2472). */
|
||||
public static final short LOCAL_NOTIFICATION = (short) 0x09A8; // 2472
|
||||
|
||||
/** Quicklook / glance (raise-to-wake) status (type 0x097A = 2426). */
|
||||
public static final short GLANCE_STATUS = (short) 0x097A; // 2426
|
||||
|
||||
/** Screen luminosity mode + level (type 0x0937 = 2359), 2-byte payload: [mode][level]. */
|
||||
public static final short LUMINOSITY_LEVEL = (short) 0x0937; // 2359
|
||||
|
||||
/** Tracker move-hands status (type 0x09BB = 2491), 1-byte payload: 0=off, 1=on. */
|
||||
public static final short TRACKER_MOVE_HANDS = (short) 0x09BB; // 2491
|
||||
|
||||
/** Tracker wear position (type 0x012F = 303), 1-byte payload: 2=left wrist, 3=right wrist. */
|
||||
public static final short TRACKER_WEAR_POS = 303; // 0x012F
|
||||
|
||||
private WithingsStructureType() {}
|
||||
}
|
||||
|
||||
+21
@@ -48,6 +48,7 @@ public final class WithingsMessageType {
|
||||
public static final short GET_ALARM_SETTINGS = 298;
|
||||
public static final short SET_ALARM = 325;
|
||||
public static final short GET_ALARM = 293;
|
||||
public static final short GET_MULTI_ALARM = 326;
|
||||
public static final short GET_ALARM_ENABLED = 2330;
|
||||
public static final short SET_ALARM_ENABLED = 2331;
|
||||
public static final short GET_ANCS_STATUS = 2353;
|
||||
@@ -85,5 +86,25 @@ public final class WithingsMessageType {
|
||||
*/
|
||||
public static final short SET_LOCAL_NOTIFICATIONS = (short) 0x0990; // 2448
|
||||
|
||||
/** Get quicklook / glance (raise-to-wake) status (cmd 0x097B). */
|
||||
public static final short GLANCE_GET = (short) 0x097B; // 2427
|
||||
/** Set quicklook / glance (raise-to-wake) status (cmd 0x0971). */
|
||||
public static final short GLANCE_SET = (short) 0x0971; // 2417
|
||||
|
||||
/** Set screen luminosity mode & level (cmd 0x0941). */
|
||||
public static final short SET_LUMINOSITY_LEVEL = (short) 0x0941; // 2369
|
||||
/** Get screen luminosity mode & level (cmd 0x0942). */
|
||||
public static final short GET_LUMINOSITY_LEVEL = (short) 0x0942; // 2370
|
||||
|
||||
/** Set tracker move-hands (move hands to 10:10 when screen turns on) (cmd 0x09AB). */
|
||||
public static final short SET_TRACKER_MOVE_HANDS = (short) 0x09AB; // 2475
|
||||
/** Get tracker move-hands status (cmd 0x09AC). */
|
||||
public static final short GET_TRACKER_MOVE_HANDS = (short) 0x09AC; // 2476
|
||||
|
||||
/** Set tracker wear position (left/right wrist) (cmd 0x014F). */
|
||||
public static final short SET_TRACKER_WEAR_POS = 335; // 0x014F
|
||||
/** Get tracker wear position (cmd 0x0150). */
|
||||
public static final short GET_TRACKER_WEAR_POS = 336; // 0x0150
|
||||
|
||||
private WithingsMessageType() {}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,15 @@
|
||||
android:inputType="number"
|
||||
android:hint="@string/alarm_smart_wakeup_interval_default" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_smart_wakeup_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TimePicker
|
||||
android:id="@+id/time_picker"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -5,24 +5,64 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- Mode toggle: App dial vs Watch crown -->
|
||||
<RadioGroup
|
||||
android:id="@+id/withings_calibration_mode_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/withings_calibration_mode_app"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/withings_calibration_mode_app"
|
||||
android:checked="true" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/withings_calibration_mode_crown"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:text="@string/withings_calibration_mode_crown" />
|
||||
</RadioGroup>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/withings_calibration_textview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/withings_calibration_mode_group"
|
||||
android:padding="30dp"
|
||||
android:text="@string/withings_calibration_text_hours"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge" />
|
||||
|
||||
<!-- App dial mode: RotaryControl -->
|
||||
<nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.RotaryControl
|
||||
android:id="@+id/rotary_control"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/withings_calibration_textview"
|
||||
android:layout_above="@id/withings_calibration_button_previous"
|
||||
android:padding="30dp"
|
||||
rotaryControl:controlpoint_color="@color/accent"
|
||||
rotaryControl:controlpoint_size="20dp"
|
||||
rotaryControl:line_color="@color/design_default_color_on_secondary"
|
||||
rotaryControl:line_thickness="10dp" />
|
||||
|
||||
<!-- Crown mode: Confirm button (hidden by default) -->
|
||||
<Button
|
||||
android:id="@+id/withings_calibration_button_confirm"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:padding="24dp"
|
||||
android:text="@string/withings_calibration_confirm"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/withings_calibration_button_previous"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -55,4 +95,4 @@
|
||||
android:padding="16dp"
|
||||
android:text="@string/ok" />
|
||||
|
||||
</RelativeLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -1914,6 +1914,17 @@
|
||||
<item>9</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_withings_scanwatch_respiratory_scan_entries">
|
||||
<item>@string/withings_scanwatch_pref_respiratory_scan_off</item>
|
||||
<item>@string/withings_scanwatch_pref_respiratory_scan_automatic</item>
|
||||
<item>@string/withings_scanwatch_pref_respiratory_scan_always</item>
|
||||
</string-array>
|
||||
<string-array name="pref_withings_scanwatch_respiratory_scan_values">
|
||||
<item>off</item>
|
||||
<item>automatic</item>
|
||||
<item>always</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_neo_display_items">
|
||||
<item>@string/menuitem_pai</item>
|
||||
<item>@string/menuitem_dnd</item>
|
||||
|
||||
@@ -3734,7 +3734,13 @@
|
||||
<string name="loyalty_cards_syncing">Syncing %d loyalty cards to device</string>
|
||||
<string name="withings_calibration_text_hours">Please use the dial below to align the hour hand to the 12.</string>
|
||||
<string name="withings_calibration_text_minutes">Now use the dial to align the minute hand to the 12.</string>
|
||||
<string name="withings_calibration_text_activity_target">Finally align the activity hand to 100%. Please be aware that this hand only moves clockwise.</string>
|
||||
<string name="withings_calibration_text_activity_target">Finally align the activity hand to 100%.</string>
|
||||
<string name="withings_calibration_crown_text_hours">Use the crown on your watch to move the hour hand to the 12.</string>
|
||||
<string name="withings_calibration_crown_text_minutes">Now use the crown to move the minute hand to the 12.</string>
|
||||
<string name="withings_calibration_crown_text_activity_target">Finally use the crown to move the activity hand to 100%.</string>
|
||||
<string name="withings_calibration_mode_app">App dial</string>
|
||||
<string name="withings_calibration_mode_crown">Watch crown</string>
|
||||
<string name="withings_calibration_confirm">Confirm</string>
|
||||
<string name="withings_bt_calibration_previous">Previous</string>
|
||||
<string name="withings_bt_calibration_next">Next</string>
|
||||
<string name="withings_scanwatch_pref_screens_title">Watch screens</string>
|
||||
@@ -3756,8 +3762,29 @@
|
||||
<!-- ECG and AFib health feature settings -->
|
||||
<string name="withings_scanwatch_pref_ecg_title">ECG Measurement</string>
|
||||
<string name="withings_scanwatch_pref_ecg_summary">Enable ECG measurement feature on the watch</string>
|
||||
<string name="withings_scanwatch_pref_afib_title">AFib Detection</string>
|
||||
<string name="withings_scanwatch_pref_afib_summary">Enable atrial fibrillation detection and night AFib monitoring</string>
|
||||
<string name="withings_scanwatch_pref_respiratory_scan_title">Respiratory Scan</string>
|
||||
<string name="withings_scanwatch_pref_respiratory_scan_off">Off</string>
|
||||
<string name="withings_scanwatch_pref_respiratory_scan_automatic">Automatic (scans some nights, minimal battery impact)</string>
|
||||
<string name="withings_scanwatch_pref_respiratory_scan_always">Always (scans every night, higher battery usage)</string>
|
||||
<string name="withings_scanwatch_pref_afib_day_title">AFib Detection (Daytime)</string>
|
||||
<string name="withings_scanwatch_pref_afib_day_summary">Enable daytime atrial fibrillation detection</string>
|
||||
<string name="withings_scanwatch_pref_afib_night_title">AFib Detection (Night)</string>
|
||||
<string name="withings_scanwatch_pref_afib_night_summary">Enable night-time atrial fibrillation detection</string>
|
||||
<string name="withings_scanwatch_alarm_smart_wakeup_description">The watch will find the best moment to wake you up between the smart wakeup window start and your alarm time, based on your sleep cycle. Set the window to 0 to wake up exactly at the alarm time.</string>
|
||||
|
||||
<!-- Quicklook / glance (raise-to-wake) -->
|
||||
<string name="withings_scanwatch_pref_quicklook_title">Quicklook</string>
|
||||
<string name="withings_scanwatch_pref_quicklook_summary">Show time when you raise your wrist</string>
|
||||
|
||||
<!-- Brightness / luminosity -->
|
||||
<string name="withings_scanwatch_pref_auto_brightness_title">Auto brightness</string>
|
||||
<string name="withings_scanwatch_pref_auto_brightness_summary">Automatically adjust screen brightness</string>
|
||||
<string name="withings_scanwatch_pref_brightness_level_title">Brightness level</string>
|
||||
<string name="withings_scanwatch_pref_brightness_level_summary">Manual brightness level (0-100)</string>
|
||||
|
||||
<!-- Move hands (move hands to 10:10 when screen turns on) -->
|
||||
<string name="withings_scanwatch_pref_move_hands_title">Move hands</string>
|
||||
<string name="withings_scanwatch_pref_move_hands_summary">Move watch hands to 10:10 when the screen turns on to avoid blocking the display</string>
|
||||
|
||||
<!-- Shortcut (long-press crown) settings -->
|
||||
<string name="withings_scanwatch_pref_shortcut_title">Long-press crown shortcut</string>
|
||||
|
||||
@@ -52,13 +52,70 @@
|
||||
android:summary="@string/withings_scanwatch_pref_ecg_summary"
|
||||
android:title="@string/withings_scanwatch_pref_ecg_title" />
|
||||
|
||||
<!-- AFib detection -->
|
||||
<!-- Respiratory scan (SpO2 during sleep) -->
|
||||
<ListPreference
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:defaultValue="off"
|
||||
android:entries="@array/pref_withings_scanwatch_respiratory_scan_entries"
|
||||
android:entryValues="@array/pref_withings_scanwatch_respiratory_scan_values"
|
||||
android:key="withings_scanwatch_respiratory_scan"
|
||||
android:persistent="true"
|
||||
android:title="@string/withings_scanwatch_pref_respiratory_scan_title" />
|
||||
|
||||
<!-- AFib daytime detection -->
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:defaultValue="false"
|
||||
android:key="withings_scanwatch_afib_enabled"
|
||||
android:key="withings_scanwatch_afib_day_enabled"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_afib_summary"
|
||||
android:title="@string/withings_scanwatch_pref_afib_title" />
|
||||
android:summary="@string/withings_scanwatch_pref_afib_day_summary"
|
||||
android:title="@string/withings_scanwatch_pref_afib_day_title" />
|
||||
|
||||
<!-- AFib night detection -->
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/ic_heartrate"
|
||||
android:defaultValue="false"
|
||||
android:key="withings_scanwatch_afib_night_enabled"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_afib_night_summary"
|
||||
android:title="@string/withings_scanwatch_pref_afib_night_title" />
|
||||
|
||||
<!-- Quicklook / glance (raise-to-wake) -->
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/ic_always_on_display"
|
||||
android:defaultValue="false"
|
||||
android:key="withings_scanwatch_quicklook"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_quicklook_summary"
|
||||
android:title="@string/withings_scanwatch_pref_quicklook_title" />
|
||||
|
||||
<!-- Auto brightness toggle -->
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/ic_brightness_2"
|
||||
android:defaultValue="true"
|
||||
android:key="withings_scanwatch_auto_brightness"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_auto_brightness_summary"
|
||||
android:title="@string/withings_scanwatch_pref_auto_brightness_title" />
|
||||
|
||||
<!-- Manual brightness level (only meaningful when auto brightness is OFF) -->
|
||||
<EditTextPreference
|
||||
android:icon="@drawable/ic_brightness_2"
|
||||
android:defaultValue="100"
|
||||
android:inputType="number"
|
||||
android:key="withings_scanwatch_brightness_level"
|
||||
android:maxLength="3"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_brightness_level_summary"
|
||||
android:title="@string/withings_scanwatch_pref_brightness_level_title" />
|
||||
|
||||
<!-- Move hands to 10:10 when screen turns on -->
|
||||
<SwitchPreference
|
||||
android:icon="@drawable/ic_sensor_calibration"
|
||||
android:defaultValue="false"
|
||||
android:key="withings_scanwatch_move_hands"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_move_hands_summary"
|
||||
android:title="@string/withings_scanwatch_pref_move_hands_title" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
|
||||
Reference in New Issue
Block a user