devices = GBApplication.app().getDeviceManager().getSelectedDevices();
for(GBDevice device : devices){
- if(device.getType() == DeviceType.WITHINGS_STEEL_HR ){
+ if(device.getType() == DeviceType.WITHINGS_STEEL_HR || device.getType() == DeviceType.WITHINGS_SCANWATCH){
this.device = device;
break;
}
@@ -78,14 +78,14 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
initView();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
- localBroadcastManager.sendBroadcast(new Intent(WithingsSteelHRDeviceSupport.START_HANDS_CALIBRATION_CMD));
+ localBroadcastManager.sendBroadcast(new Intent(WithingsBaseDeviceSupport.START_HANDS_CALIBRATION_CMD));
}
@Override
protected void onDestroy() {
super.onDestroy();
if (localBroadcastManager != null) {
- localBroadcastManager.sendBroadcast(new Intent(WithingsSteelHRDeviceSupport.STOP_HANDS_CALIBRATION_CMD));
+ localBroadcastManager.sendBroadcast(new Intent(WithingsBaseDeviceSupport.STOP_HANDS_CALIBRATION_CMD));
}
}
@@ -99,7 +99,7 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
rotaryControl.setRotationListener(new RotaryControl.RotationListener() {
@Override
public void onRotation(short movementAmount) {
- Intent calibration = new Intent(WithingsSteelHRDeviceSupport.HANDS_CALIBRATION_CMD);
+ Intent calibration = new Intent(WithingsBaseDeviceSupport.HANDS_CALIBRATION_CMD);
calibration.putExtra("hand", hands[handIndex].code);
calibration.putExtra("movementAmount", movementAmount);
localBroadcastManager.sendBroadcast(calibration);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractWithingsActivitySample.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractWithingsActivitySample.java
new file mode 100644
index 0000000000..609e53ffa2
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/entities/AbstractWithingsActivitySample.java
@@ -0,0 +1,33 @@
+/* Copyright (C) 2023-2024 Frank Ertl
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.entities;
+
+/**
+ * Common abstract base for Withings device activity samples (Steel HR and Scanwatch).
+ * Provides access to Withings-specific fields shared by all Withings devices:
+ * duration, distance, and calories.
+ */
+public abstract class AbstractWithingsActivitySample extends AbstractActivitySample {
+ public abstract int getDuration();
+ public abstract void setDuration(int duration);
+
+ public abstract int getDistance();
+ public abstract void setDistance(int distance);
+
+ public abstract int getCalories();
+ public abstract void setCalories(int calories);
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index f1f7fc1fb0..2249de5a4c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -422,6 +422,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoo
import nodomain.freeyourgadget.gadgetbridge.devices.waspos.WaspOSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watch9.Watch9DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.withingsscanwatch.WithingsScanwatchDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watches.MiBand10Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watches.MiBand4CCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.watches.MiBand7ProCoordinator;
@@ -859,6 +860,7 @@ public enum DeviceType {
REALME_BUDS_AIR_5_PRO(RealmeBudsAir5ProCoordinator.class),
SOFLOW_SO6(SoFlowCoordinator.class),
WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class),
+ WITHINGS_SCANWATCH(WithingsScanwatchDeviceCoordinator.class),
SONY_WENA_3(SonyWena3Coordinator.class),
FEMOMETER_VINCA2(FemometerVinca2DeviceCoordinator.class),
PIXOO(PixooCoordinator.class),
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingsscanwatch/WithingsScanwatchDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingsscanwatch/WithingsScanwatchDeviceSupport.java
new file mode 100644
index 0000000000..7eb9327da9
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingsscanwatch/WithingsScanwatchDeviceSupport.java
@@ -0,0 +1,193 @@
+/* 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingsscanwatch;
+
+import android.content.SharedPreferences;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.withingsscanwatch.WithingsScanwatchSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractWithingsActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+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.datastructures.EndOfTransmission;
+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.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;
+
+/**
+ * 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).
+ * All protocol logic is in {@link WithingsBaseDeviceSupport}.
+ */
+public class WithingsScanwatchDeviceSupport extends WithingsBaseDeviceSupport {
+
+ private static final Logger logger = LoggerFactory.getLogger(WithingsScanwatchDeviceSupport.class);
+
+ static final String PREF_SCREENS_SORTABLE = "withings_scanwatch_screens_sortable";
+ private static final String PREF_SCREENS_LAST_SENT = "withings_scanwatch_screens_last_sent";
+
+ @Override
+ protected WithingsUUIDs getWithingsUUIDs() {
+ return WithingsUUIDs.SCANWATCH;
+ }
+
+ @Override
+ public AbstractSampleProvider extends AbstractWithingsActivitySample> createSampleProvider(GBDevice device, DaoSession session) {
+ return new WithingsScanwatchSampleProvider(device, session);
+ }
+
+ private static final String PREF_SHORTCUT_ACTION = "withings_scanwatch_shortcut_action";
+
+ @Override
+ protected void addExtraSyncCommands() {
+ addSimpleConversationToQueue(
+ new WithingsMessage(WithingsMessageType.GET_SHORTCUT),
+ new GetShortcutHandler(this)
+ );
+ }
+
+ @Override
+ protected boolean handleExtraConfiguration(String config) {
+ if (PREF_SCREENS_SORTABLE.equals(config)) {
+ clearQueue();
+ addScreenListCommands();
+ sendQueue();
+ return true;
+ }
+ if (PREF_SHORTCUT_ACTION.equals(config)) {
+ final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
+ final String val = prefs.getString(PREF_SHORTCUT_ACTION, "0");
+ final byte action;
+ try {
+ action = Byte.parseByte(val);
+ } catch (NumberFormatException e) {
+ logger.warn("Invalid shortcut action value: {}", val);
+ return true;
+ }
+ final WithingsMessage msg = new WithingsMessage(WithingsMessageType.SET_SHORTCUT);
+ msg.addDataStructure(new ShortcutAction(action));
+ clearQueue();
+ addSimpleConversationToQueue(msg);
+ sendQueue();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Builds the Scanwatch screen list from the user's drag-sort preference.
+ *
+ * The preference {@code withings_scanwatch_screens_sortable} stores a comma-separated list
+ * of screen value keys in the order the user has arranged them. Only screens present in the
+ * list are enabled; removing an entry from the list disables that screen on the watch.
+ *
+ *
Each screen has a fixed internal slot number (confirmed from {@code reorder_screens.zip}
+ * BLE capture). The watch determines display order by ascending slot number, so the slot values
+ * are fixed per screen - reordering is achieved by which screens are included, not by changing
+ * slot numbers.
+ *
+ *
Screen ID <-> slot mapping is defined in {@link WithingsScreenId#getScanwatchSlot(int)}.
+ *
+ *
This method is a no-op if the current screen list matches what was last successfully sent
+ * to the watch.
+ */
+ @Override
+ protected void addScreenListCommands() {
+ final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
+
+ final List defaultScreens = Arrays.asList(
+ getContext().getResources().getStringArray(R.array.pref_withings_scanwatch_screens_default));
+
+ final String screensPref = prefs.getString(PREF_SCREENS_SORTABLE, null);
+ final List enabledScreens;
+ if (screensPref == null || screensPref.isEmpty()) {
+ enabledScreens = defaultScreens;
+ } else {
+ enabledScreens = Arrays.asList(screensPref.split(","));
+ }
+
+ // Normalise to a canonical comma-separated string for comparison
+ final String currentValue = String.join(",", enabledScreens);
+ final String lastSentValue = prefs.getString(PREF_SCREENS_LAST_SENT, null);
+
+ if (Objects.equals(currentValue, lastSentValue)) {
+ logger.debug("Screen list unchanged ({}), skipping SET_SCREEN_LIST", currentValue);
+ return;
+ }
+
+ Message message = new WithingsMessage(WithingsMessageType.SET_SCREEN_LIST);
+ for (final String screenKey : enabledScreens) {
+ final int screenId = screenKeyToId(screenKey);
+ if (screenId < 0) {
+ logger.warn("Unknown screen key '{}', skipping", screenKey);
+ continue;
+ }
+ final byte slot = WithingsScreenId.getScanwatchSlot(screenId);
+ if (slot < 0) {
+ logger.warn("No fixed slot for screen key '{}' (id=0x{:04x}), skipping", screenKey, screenId);
+ continue;
+ }
+ message.addDataStructure(buildScreen(screenId, slot));
+ }
+ message.addDataStructure(new EndOfTransmission());
+ addSimpleConversationToQueue(message);
+
+ // Record what we sent so we can skip on future syncs if nothing changed
+ prefs.edit().putString(PREF_SCREENS_LAST_SENT, currentValue).apply();
+ }
+
+ /**
+ * Maps a screen preference value key (as stored in the DragSortListPreference) to the
+ * corresponding {@link WithingsScreenId} constant.
+ *
+ * @param key the string value from {@code pref_withings_scanwatch_screens_values}
+ * @return the screen ID constant, or {@code -1} if the key is not recognised
+ */
+ private static int screenKeyToId(final String key) {
+ switch (key) {
+ case "date": return WithingsScreenId.DATE;
+ case "sleep": return WithingsScreenId.SLEEP;
+ case "ecg": return WithingsScreenId.ECG;
+ case "elevation": return WithingsScreenId.ELEVATION;
+ case "heart_rate": return WithingsScreenId.HEART_RATE;
+ case "spo2": return WithingsScreenId.SPO2;
+ case "calories": return WithingsScreenId.CALORIES;
+ case "settings": return WithingsScreenId.SETTINGS;
+ case "workouts": return WithingsScreenId.WORKOUTS;
+ case "distance": return WithingsScreenId.DISTANCE;
+ case "steps": return WithingsScreenId.STEPS;
+ case "breathe": return WithingsScreenId.BREATHE;
+ case "clock": return WithingsScreenId.CLOCK;
+ default: return -1;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java
index d290d8fef0..d74b99f8a8 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java
@@ -46,10 +46,10 @@ public class AuthenticationHandler extends AbstractResponseHandler {
// TODO: Save this somewhere if we actually decide to use te secret for more security:
private final String secret = "2EM5zNP37QzM00hmP6BFTD92nG15XwNd";
- private WithingsSteelHRDeviceSupport support;
+ private WithingsBaseDeviceSupport support;
private Challenge challengeToSend;
- public AuthenticationHandler(WithingsSteelHRDeviceSupport support) {
+ public AuthenticationHandler(WithingsBaseDeviceSupport support) {
super(support);
this.support = support;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsBaseDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsBaseDeviceSupport.java
new file mode 100644
index 0000000000..207e7bc14c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsBaseDeviceSupport.java
@@ -0,0 +1,817 @@
+/* Copyright (C) 2023-2024 Ascense, Frank Ertl
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
+
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothGatt;
+import android.bluetooth.BluetoothGattCharacteristic;
+import android.bluetooth.BluetoothGattDescriptor;
+import android.bluetooth.BluetoothGattService;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.GBApplication;
+import nodomain.freeyourgadget.gadgetbridge.R;
+import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
+import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
+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.model.NotificationSpec;
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.ServerTransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUIDs;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ActivitySampleHandler;
+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.HeartRateHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ResponseHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SetupFinishedHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SimpleConversation;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SyncFinishedHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ScreenSettingsHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.WorkoutScreenListHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivityTarget;
+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.AlarmStatus;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AncsStatus;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.DataStructureFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.EndOfTransmission;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GetActivitySamples;
+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.Locale;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.MoveHand;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Probe;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ProbeOsVersion;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ScreenSettings;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsScreenId;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Time;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TypeVersion;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.User;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnit;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnitConstants;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreen;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.ExpectedResponse;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageBuilder;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.SimpleHexToByteMessage;
+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.incoming.IncomingMessageHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.IncomingMessageHandlerFactory;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.LiveWorkoutHandler;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributes;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributesResponse;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationProvider;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationSource;
+import nodomain.freeyourgadget.gadgetbridge.util.GB;
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+
+import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE;
+import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO;
+
+public abstract class WithingsBaseDeviceSupport extends AbstractBTLESingleDeviceSupport {
+
+ private static final Logger logger = LoggerFactory.getLogger(WithingsBaseDeviceSupport.class);
+ public static final String LAST_ACTIVITY_SYNC = "lastActivitySync";
+ public static final String HANDS_CALIBRATION_CMD = "withings_hands_calibration";
+ public static final String START_HANDS_CALIBRATION_CMD = "start_withings_hands_calibration";
+ public static final String STOP_HANDS_CALIBRATION_CMD = "stop_withings_hands_calibration";
+ private final MessageBuilder messageBuilder;
+ private LiveWorkoutHandler liveWorkoutHandler;
+ private ActivitySampleHandler activitySampleHandler;
+ private final ConversationQueue conversationQueue;
+ private boolean firstTimeConnect;
+ private BluetoothGattCharacteristic notificationSourceCharacteristic;
+ private BluetoothGattCharacteristic dataSourceCharacteristic;
+ private BluetoothDevice device;
+
+ /**
+ * Creates a device-specific sample provider for storing activity data.
+ * Subclasses return the provider appropriate for their database table.
+ */
+ public abstract nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider extends nodomain.freeyourgadget.gadgetbridge.entities.AbstractWithingsActivitySample> createSampleProvider(nodomain.freeyourgadget.gadgetbridge.impl.GBDevice device, nodomain.freeyourgadget.gadgetbridge.entities.DaoSession session);
+
+ /**
+ * Returns the BLE UUID set for this device variant.
+ * Steel HR uses suffix "0037"; Scanwatch uses suffix "005d".
+ */
+ protected abstract WithingsUUIDs getWithingsUUIDs();
+ private boolean syncInProgress;
+ private final ActivityUser activityUser;
+ private final NotificationProvider notificationProvider;
+ private final IncomingMessageHandlerFactory incomingMessageHandlerFactory;
+ private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper());
+
+ public WithingsBaseDeviceSupport() {
+ super(logger);
+ conversationQueue = new ConversationQueue(this);
+ notificationProvider = NotificationProvider.getInstance(this);
+ messageBuilder = new MessageBuilder(this, new MessageFactory(new DataStructureFactory()));
+ liveWorkoutHandler = new LiveWorkoutHandler(this);
+ incomingMessageHandlerFactory = IncomingMessageHandlerFactory.getInstance(this);
+ addSupportedService(getWithingsUUIDs().WITHINGS_SERVICE_UUID);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
+ addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
+ addANCSService();
+ activityUser = new ActivityUser();
+
+ IntentFilter commandFilter = new IntentFilter(HANDS_CALIBRATION_CMD);
+ commandFilter.addAction(START_HANDS_CALIBRATION_CMD);
+ commandFilter.addAction(STOP_HANDS_CALIBRATION_CMD);
+ final BroadcastReceiver commandReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction() == null) {
+ return;
+ }
+
+ switch (intent.getAction()) {
+ case HANDS_CALIBRATION_CMD:
+ MoveHand moveHand = new MoveHand();
+ moveHand.setHand(intent.getShortExtra("hand", (short) 1));
+ moveHand.setMovement(intent.getShortExtra("movementAmount", (short) 1));
+ sendToDevice(new WithingsMessage(WithingsMessageType.MOVE_HAND, moveHand));
+ break;
+ case START_HANDS_CALIBRATION_CMD:
+ sendToDevice(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION));
+ break;
+ case STOP_HANDS_CALIBRATION_CMD:
+ sendToDevice(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION));
+ break;
+ }
+ }
+ };
+
+ LocalBroadcastManager.getInstance(GBApplication.getContext()).registerReceiver(commandReceiver, commandFilter);
+ }
+
+ @Override
+ public void dispose() {
+ synchronized (ConnectionMonitor) {
+ backgroundTasksHandler.removeCallbacksAndMessages(null);
+
+ super.dispose();
+ }
+ }
+
+ @Override
+ protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
+ logger.debug("Starting initialization...");
+ conversationQueue.clear();
+ builder.setDeviceState(GBDevice.State.INITIALIZING);
+ getDevice().setFirmwareVersion("N/A");
+ getDevice().setFirmwareVersion2("N/A");
+
+ // Delay initialization with 2 seconds to give the watch time to settle
+ backgroundTasksHandler.removeCallbacksAndMessages(null);
+ backgroundTasksHandler.postDelayed(this::postConnectInitialization, 2000);
+
+ return builder;
+ }
+
+ private void postConnectInitialization() {
+ final TransactionBuilder builder = createTransactionBuilder("delayed initialization");
+ builder.notify(getWithingsUUIDs().WITHINGS_WRITE_CHARACTERISTIC_UUID, true);
+ builder.requestMtu(512);
+ builder.queue();
+ }
+
+ @Override
+ public boolean connectFirstTime() {
+ firstTimeConnect = true;
+ return connect();
+ }
+
+ @Override
+ public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
+ super.onMtuChanged(gatt, mtu, status);
+ if (status != BluetoothGatt.GATT_SUCCESS) {
+ logger.error("Failed to change mtu - disconnecting");
+ disconnect();
+ return;
+ }
+
+ logger.debug("MTU has changed to {}", mtu);
+ if (firstTimeConnect) {
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, getLocale()));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION));
+ 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.GET_ANCS_STATUS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true)));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SETUP_FINISHED), new SetupFinishedHandler(this));
+ } else {
+ Message message = new WithingsMessage(WithingsMessageType.PROBE);
+ message.addDataStructure(new Probe((short) 1, (short) 1, 5100401));
+ message.addDataStructure(new ProbeOsVersion((short) Build.VERSION.SDK_INT));
+ conversationQueue.clear();
+ addSimpleConversationToQueue(message, new AuthenticationHandler(this));
+ }
+
+ if (!firstTimeConnect) {
+ finishInitialization();
+ }
+ conversationQueue.send();
+ }
+
+ public void doSync() {
+ activitySampleHandler = new ActivitySampleHandler(this);
+ conversationQueue.clear();
+ try {
+ if (syncInProgress) {
+ return;
+ }
+
+ getDevice().setBusyTask(R.string.busy_task_syncing, getContext());
+ syncInProgress = true;
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time()));
+
+ if (shoudSync()) {
+ logger.debug("Doing full sync...");
+ WithingsMessage message = new WithingsMessage(WithingsMessageType.SET_USER);
+ message.addDataStructure(getUser());
+ // The UserSecret appears in the original communication with the HealthMate app. Until now GB works without the secret.
+ // 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_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_ALARM_ENABLED));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_WORKOUT_SCREEN_LIST), new WorkoutScreenListHandler(this));
+ addExtraSyncCommands();
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(getLastSyncTimestamp());
+ message = new WithingsMessage(WithingsMessageType.GET_ACTIVITY_SAMPLES, ExpectedResponse.EOT);
+ message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
+ addSimpleConversationToQueue(message, activitySampleHandler);
+ message = new WithingsMessage(WithingsMessageType.GET_MOVEMENT_SAMPLES, ExpectedResponse.EOT);
+ message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
+ message.addDataStructure(new TypeVersion());
+ addSimpleConversationToQueue(message, activitySampleHandler);
+ message = new WithingsMessage(WithingsMessageType.GET_HEARTRATE_SAMPLES, ExpectedResponse.EOT);
+ message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
+ message.addDataStructure(new TypeVersion());
+ addSimpleConversationToQueue(message, activitySampleHandler);
+ }
+ } catch (Exception e) {
+ logger.error("Could not synchronize! ", e);
+ conversationQueue.clear();
+ } finally {
+ // This must be done in all cases or the watch won't respond anymore!
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SYNC_OK), new SyncFinishedHandler(this));
+ }
+ conversationQueue.send();
+ }
+
+
+ @Override
+ public boolean onCharacteristicChanged(BluetoothGatt gatt,
+ BluetoothGattCharacteristic characteristic,
+ byte[] data) {
+ if (super.onCharacteristicChanged(gatt, characteristic, data)) {
+ return true;
+ }
+
+ logger.debug("onCharacteristicChanged raw: {}", nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump(data));
+ boolean complete = messageBuilder.buildMessage(data);
+ if (complete) {
+ Message message = messageBuilder.getMessage();
+ if (message.isIncomingMessage()) {
+ logger.debug("received incoming message: {}", message.getType());
+ IncomingMessageHandler handler = incomingMessageHandlerFactory.getHandler(message);
+ if (handler == null) {
+ logger.warn("No handler for incoming message type={}", message.getType());
+ } else {
+ handler.handleMessage(message);
+ }
+ } else {
+ logger.debug("received response message: type={} (0x{})", message.getType(), Integer.toHexString(message.getType() & 0xffff));
+ conversationQueue.processResponse(message);
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onSetCallState(CallSpec callSpec) {
+ if (callSpec.command == CallSpec.CALL_INCOMING) {
+ NotificationSpec notificationSpec = new NotificationSpec();
+ notificationSpec.sourceAppId = "incoming.call";
+ notificationSpec.title = callSpec.number;
+ notificationSpec.sender = callSpec.name;
+ notificationSpec.type = NotificationType.GENERIC_PHONE;
+ notificationProvider.notifyClient(notificationSpec);
+ } else {
+ logger.info("Received yet unhandled call command: " + callSpec.command);
+ }
+ }
+
+ @Override
+ public void onNotification(NotificationSpec notificationSpec) {
+ notificationProvider.notifyClient(notificationSpec);
+ }
+
+ @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));
+ for (Alarm alarm : alarms) {
+ if (alarm.getEnabled() && !alarm.getUnused()) {
+ noAlarmsEnabled = false;
+ addAlarm(alarm);
+ }
+ }
+
+ if (noAlarmsEnabled) {
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(false)));
+ }
+
+ conversationQueue.send();
+ }
+
+ @Override
+ public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
+ this.device = device;
+ return true;
+ }
+
+ @Override
+ public boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
+ if (characteristic.getUuid().equals(getWithingsUUIDs().CONTROL_POINT_CHARACTERISTIC_UUID)) {
+ logger.debug("Got GetNotificationAttributesRequest: " + GB.hexdump(value));
+ GetNotificationAttributes request = new GetNotificationAttributes();
+ request.deserialize(value);
+ notificationProvider.handleNotificationAttributeRequest(request);
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onFetchRecordedData(int dataTypes) {
+ doSync();
+ }
+
+ @Override
+ public void onHeartRateTest() {
+ conversationQueue.clear();
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_HR), new HeartRateHandler(this));
+ conversationQueue.send();
+ }
+
+ @Override
+ public void onSendConfiguration(String config) {
+ try {
+ switch (config) {
+ case HuamiConst.PREF_WORKOUT_ACTIVITY_TYPES_SORTABLE:
+ setWorkoutActivityTypes();
+ break;
+ case PREF_LANGUAGE:
+ setLanguage();
+ break;
+ default:
+ if (!handleExtraConfiguration(config)) {
+ logger.debug("unknown configuration setting received: " + config);
+ }
+ }
+ } catch (Exception e) {
+ GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+
+ /**
+ * Called from {@link #onSendConfiguration(String)} for device-specific configuration keys
+ * not handled by the base class.
+ *
+ * @param config the configuration key
+ * @return {@code true} if the config was handled, {@code false} otherwise
+ */
+ protected boolean handleExtraConfiguration(String config) {
+ return false;
+ }
+
+ @Override
+ public void onTestNewFunction() {
+ String hexMessage = "0105080015050900111006040102030507000000000000000000";
+ conversationQueue.clear();
+ addSimpleConversationToQueue(new SimpleHexToByteMessage(hexMessage));
+ conversationQueue.send();
+ }
+
+ @Override
+ public boolean useAutoConnect() {
+ return false;
+ }
+
+ public void sendToDevice(Message message) {
+ if (message == null) {
+ return;
+ }
+
+ try {
+ TransactionBuilder builder = createTransactionBuilder("conversation");
+ builder.setCallback(this);
+ BluetoothGattCharacteristic characteristic = getCharacteristic(getWithingsUUIDs().WITHINGS_WRITE_CHARACTERISTIC_UUID);
+ if (characteristic == null) {
+ logger.info("Characteristic with UUID " + getWithingsUUIDs().WITHINGS_WRITE_CHARACTERISTIC_UUID + " not found.");
+ return;
+ }
+
+ byte[] rawData = message.getRawData();
+ builder.writeChunkedData(characteristic, rawData, getMTU() - 3);
+ builder.queue();
+ } catch (Exception e) {
+ logger.warn("Could not send message because of " + e.getMessage());
+ }
+ }
+
+ public void sendAncsNotificationSourceNotification(NotificationSource notificationSource) {
+ try {
+ ServerTransactionBuilder builder = performServer("notificationSourceNotification");
+ byte[] data = notificationSource.serialize();
+ builder.notifyCharacteristicChanged(device, notificationSourceCharacteristic, data);
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ logger.error("Could not send notification.", e);
+ GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+
+ public void sendAncsDataSourceNotification(GetNotificationAttributesResponse response) {
+ try {
+ ServerTransactionBuilder builder = performServer("dataSourceNotification");
+ byte[] data = response.serialize();
+ builder.notifyCharacteristicChanged(device, dataSourceCharacteristic, data);
+ builder.queue(getQueue());
+ } catch (IOException e) {
+ logger.error("Could not send notification.", e);
+ GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e);
+ }
+ }
+
+ public void finishInitialization() {
+ TransactionBuilder builder = createTransactionBuilder("setupFinished");
+ builder.setDeviceState(GBDevice.State.INITIALIZED);
+ builder.queue();
+ logger.debug("Finished initialization.");
+ }
+
+ public void finishSync() {
+ syncInProgress = false;
+ if (getDevice().isBusy()) {
+ getDevice().unsetBusyTask();
+ getDevice().sendDeviceUpdateIntent(getContext());
+ }
+ activitySampleHandler.onSyncFinished();
+ saveLastSyncTimestamp(new Date().getTime());
+ }
+
+ void onAuthenticationFinished() {
+ if (!firstTimeConnect) {
+ doSync();
+ } else {
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true)));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
+ conversationQueue.send();
+ }
+ }
+
+ private void addAlarm(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);
+ }
+
+ Message alarmMessage = new WithingsMessage(WithingsMessageType.SET_ALARM, alarmSettings);
+ if (!StringUtils.isEmpty(alarm.getTitle())) {
+ AlarmName alarmName = new AlarmName(alarm.getTitle());
+ alarmMessage.addDataStructure(alarmName);
+ }
+
+ addSimpleConversationToQueue(alarmMessage);
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(true)));
+ }
+
+ private short mapRepetitionToWithingsValue(Alarm alarm) {
+ int repetition = 0;
+ if (alarm.getRepetition(Alarm.ALARM_MON)) {
+ repetition += 0x02;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_TUE)) {
+ repetition += 0x04;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_WED)) {
+ repetition += 0x08;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_THU)) {
+ repetition += 0x10;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_FRI)) {
+ repetition += 0x20;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_SAT)) {
+ repetition += 0x40;
+ }
+ if (alarm.getRepetition(Alarm.ALARM_SUN)) {
+ repetition += 0x01;
+ }
+
+ return (short)(repetition + 0x80);
+ }
+
+ private void addANCSService() {
+ BluetoothGattService withingsGATTService = new BluetoothGattService(getWithingsUUIDs().WITHINGS_ANCS_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
+ notificationSourceCharacteristic = new BluetoothGattCharacteristic(getWithingsUUIDs().NOTIFICATION_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
+ notificationSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(getWithingsUUIDs().CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE));
+ withingsGATTService.addCharacteristic(notificationSourceCharacteristic);
+ withingsGATTService.addCharacteristic(new BluetoothGattCharacteristic(getWithingsUUIDs().CONTROL_POINT_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE));
+ dataSourceCharacteristic = new BluetoothGattCharacteristic(getWithingsUUIDs().DATA_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
+ dataSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(getWithingsUUIDs().CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE));
+ withingsGATTService.addCharacteristic(dataSourceCharacteristic);
+ addSupportedServerService(withingsGATTService);
+ }
+
+ protected void addSimpleConversationToQueue(Message message) {
+ addSimpleConversationToQueue(message, null);
+ }
+
+ protected void addSimpleConversationToQueue(Message message, ResponseHandler handler) {
+ Conversation conversation = new SimpleConversation(handler);
+ conversation.setRequest(message);
+ conversationQueue.addConversation(conversation);
+ }
+
+ /** Clears any pending conversations from the queue. */
+ protected void clearQueue() {
+ conversationQueue.clear();
+ }
+
+ /** Starts sending the queued conversations to the device. */
+ protected void sendQueue() {
+ conversationQueue.send();
+ }
+
+ private void saveLastSyncTimestamp(@NonNull long timestamp) {
+ SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
+ editor.putLong(LAST_ACTIVITY_SYNC, timestamp);
+ editor.apply();
+ }
+
+ private long getLastSyncTimestamp() {
+ SharedPreferences settings = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
+ long lastSyncTime = settings.getLong(LAST_ACTIVITY_SYNC, 0);
+ if (lastSyncTime > 0) {
+ return lastSyncTime;
+ } else {
+ Date currentDate = new Date();
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(currentDate.getTime());
+ c.add(Calendar.HOUR, - 10);
+ return c.getTimeInMillis();
+ }
+ }
+
+ private boolean shoudSync() {
+ long lastSynced = getLastSyncTimestamp();
+ int minuteInMillis = 60 * 1000;
+ return new Date().getTime() - lastSynced > minuteInMillis;
+ }
+
+ private User getUser() {
+ User user = new User();
+ ActivityUser activityUser = new ActivityUser();
+ user.setName(activityUser.getName());
+ user.setGender((byte) activityUser.getGender());
+ user.setHeight(activityUser.getHeightCm());
+ user.setWeight(activityUser.getWeightKg());
+ user.setBirthdate(activityUser.getUserBirthday());
+ return user;
+ }
+
+ protected void addScreenListCommands() {
+ // Default screen list used by the Withings Steel HR.
+ // Subclasses (e.g. WithingsScanwatchDeviceSupport) should override this
+ // to provide a device-specific or user-configurable screen list.
+ // Screen IDs are defined in WithingsScreenId; idOnDevice is the slot/position on the watch.
+ // TODO: these IDs need to be verified via BLE packet capture (HCI snoop log).
+ Message message = new WithingsMessage(WithingsMessageType.SET_SCREEN_LIST);
+ message.addDataStructure(buildScreen(WithingsScreenId.NOTIFICATIONS, (byte) 6));
+ message.addDataStructure(buildScreen(WithingsScreenId.HEART_RATE_STEEL,(byte) 1));
+ message.addDataStructure(buildScreen(WithingsScreenId.ACTIVITY, (byte) 4));
+ message.addDataStructure(buildScreen(WithingsScreenId.CALORIES_STEEL, (byte) 2));
+ message.addDataStructure(buildScreen(WithingsScreenId.ALARM, (byte) 3));
+ message.addDataStructure(buildScreen(WithingsScreenId.CONFIG, (byte) 7));
+ message.addDataStructure(buildScreen(WithingsScreenId.CHRONOMETER, (byte) 9));
+ message.addDataStructure(new EndOfTransmission());
+ addSimpleConversationToQueue(message);
+ }
+
+ /**
+ * Convenience method: creates a {@link ScreenSettings} with the given screen ID and slot.
+ */
+ protected ScreenSettings buildScreen(int screenId, byte slotOnDevice) {
+ ScreenSettings settings = new ScreenSettings();
+ settings.setId(screenId);
+ settings.setIdOnDevice(slotOnDevice);
+ return settings;
+ }
+
+ /**
+ * Hook for subclasses to add device-specific commands to the sync queue.
+ * Called within the {@code shoudSync()} block of {@link #doSync()}, after the common
+ * commands (user, alarms, screen settings, workout screens) have been queued.
+ *
+ * The default implementation is a no-op.
+ */
+ protected void addExtraSyncCommands() {
+ // no-op by default
+ }
+
+ private void setWorkoutActivityTypes() {
+ final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
+
+ final List allActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_values));
+ final List defaultActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_default));
+ final String activityTypesPref = prefs.getString("workout_activity_types_sortable", null);
+
+ final List enabledActivityTypes;
+ if (activityTypesPref == null || activityTypesPref.equals("")) {
+ enabledActivityTypes = defaultActivityTypes;
+ } else {
+ enabledActivityTypes = Arrays.asList(activityTypesPref.split(","));
+ }
+
+ conversationQueue.clear();
+ for (int i = 0; i < enabledActivityTypes.size(); i++) {
+ String workoutType = enabledActivityTypes.get(i);
+ try {
+ Message message = createWorkoutScreenMessage(workoutType);
+ if (i == enabledActivityTypes.size() - 1) {
+ message.addDataStructure(new EndOfTransmission());
+ }
+ addSimpleConversationToQueue(message);
+ } catch (Exception e) {
+ logger.warn("exception in setWorkoutActivityTypes", e);
+ }
+ }
+
+ conversationQueue.send();
+ }
+
+ @NonNull
+ private Message createWorkoutScreenMessage(String workoutType) {
+ WithingsActivityType withingsActivityType = WithingsActivityType.fromPrefValue(workoutType);
+ int code = withingsActivityType.getCode();
+ Message message = new WithingsMessage(WithingsMessageType.SET_WORKOUT_SCREEN, ExpectedResponse.NONE);
+ WorkoutScreen workoutScreen = new WorkoutScreen();
+ workoutScreen.setId(code);
+ final int stringId = getContext().getResources().getIdentifier("activity_type_" + workoutType, "string", getContext().getPackageName());
+ workoutScreen.setName(getContext().getString(stringId));
+ message.addDataStructure(workoutScreen);
+
+ ImageMetaData imageMetaData = new ImageMetaData();
+ imageMetaData.setHeight((byte)24);
+ imageMetaData.setWidth((byte)22);
+ message.addDataStructure(imageMetaData);
+
+ ImageData imageData = new ImageData();
+ final int drawableId = withingsActivityType.toActivityKind().getIcon();
+ Drawable drawable = getContext().getDrawable(drawableId);
+ imageData.setImageData(IconHelper.getIconBytesFromDrawable(drawable));
+ message.addDataStructure(imageData);
+
+ return message;
+ }
+
+ protected void setLanguage() {
+ Locale locale = getLocale();
+
+ conversationQueue.clear();
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, locale));
+ conversationQueue.send();
+ }
+
+ private Locale getLocale() {
+ String localeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())
+ .getString(PREF_LANGUAGE, PREF_LANGUAGE_AUTO);
+
+ if (localeString.equals(PREF_LANGUAGE_AUTO)) {
+ localeString = java.util.Locale.getDefault().getLanguage();
+ }
+
+ String language = localeString.substring(0, 2);
+ switch (language) {
+ case "de":
+ case "en":
+ case "es":
+ case "fr":
+ case "it":
+ return new Locale(language);
+ default:
+ return new Locale("en");
+ }
+ }
+
+ private short getTimeMode() {
+ if ("24h".equals(getDevicePrefs().getTimeFormat())) {
+ return UserUnitConstants.UNIT_24H;
+ } else {
+ return UserUnitConstants.UNIT_12H;
+ }
+ }
+
+ private short getUnit() {
+ String units = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric));
+
+ if (units.equals(GBApplication.getContext().getString(R.string.p_unit_metric))) {
+ return UserUnitConstants.UNIT_KM;
+ } else {
+ return UserUnitConstants.UNIT_MILES;
+ }
+ }
+
+ @Override
+ public boolean getImplicitCallbackModify() {
+ return true;
+ }
+
+ @Override
+ public void onSetTime() {
+ conversationQueue.clear();
+ addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time()));
+ conversationQueue.send();
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java
index 876bb4e3fc..59352a5a50 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java
@@ -16,763 +16,26 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothGatt;
-import android.bluetooth.BluetoothGattCharacteristic;
-import android.bluetooth.BluetoothGattDescriptor;
-import android.bluetooth.BluetoothGattService;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.localbroadcastmanager.content.LocalBroadcastManager;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Date;
-import java.util.List;
-
-import nodomain.freeyourgadget.gadgetbridge.GBApplication;
-import nodomain.freeyourgadget.gadgetbridge.R;
-import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractWithingsActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
+import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
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.model.DistanceUnit;
-import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
-import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
-import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLESingleDeviceSupport;
-import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService;
-import nodomain.freeyourgadget.gadgetbridge.service.btle.ServerTransactionBuilder;
-import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUID;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ActivitySampleHandler;
-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.HeartRateHandler;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ResponseHandler;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SetupFinishedHandler;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SimpleConversation;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.SyncFinishedHandler;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.WorkoutScreenListHandler;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivityTarget;
-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.AlarmStatus;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.AncsStatus;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.DataStructureFactory;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.EndOfTransmission;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GetActivitySamples;
-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.Locale;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.MoveHand;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Probe;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ProbeOsVersion;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ScreenSettings;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Time;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.TypeVersion;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.User;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnit;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.UserUnitConstants;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreen;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.ExpectedResponse;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageBuilder;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.MessageFactory;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.SimpleHexToByteMessage;
-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.incoming.IncomingMessageHandler;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.IncomingMessageHandlerFactory;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming.LiveWorkoutHandler;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributes;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.GetNotificationAttributesResponse;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationProvider;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification.NotificationSource;
-import nodomain.freeyourgadget.gadgetbridge.util.GB;
-import nodomain.freeyourgadget.gadgetbridge.util.GBPrefs;
-import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUIDs;
-import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE;
-import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_LANGUAGE_AUTO;
+/**
+ * Device support for Withings Steel HR. All protocol logic lives in {@link WithingsBaseDeviceSupport}.
+ */
+public class WithingsSteelHRDeviceSupport extends WithingsBaseDeviceSupport {
-public class WithingsSteelHRDeviceSupport extends AbstractBTLESingleDeviceSupport {
-
- private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRDeviceSupport.class);
- public static final String LAST_ACTIVITY_SYNC = "lastActivitySync";
- public static final String HANDS_CALIBRATION_CMD = "withings_hands_calibration";
- public static final String START_HANDS_CALIBRATION_CMD = "start_withings_hands_calibration";
- public static final String STOP_HANDS_CALIBRATION_CMD = "stop_withings_hands_calibration";
- private final MessageBuilder messageBuilder;
- private LiveWorkoutHandler liveWorkoutHandler;
- private ActivitySampleHandler activitySampleHandler;
- private final ConversationQueue conversationQueue;
- private boolean firstTimeConnect;
- private BluetoothGattCharacteristic notificationSourceCharacteristic;
- private BluetoothGattCharacteristic dataSourceCharacteristic;
- private BluetoothDevice device;
- private boolean syncInProgress;
- private final ActivityUser activityUser;
- private final NotificationProvider notificationProvider;
- private final IncomingMessageHandlerFactory incomingMessageHandlerFactory;
- private final Handler backgroundTasksHandler = new Handler(Looper.getMainLooper());
-
- public WithingsSteelHRDeviceSupport() {
- super(logger);
- conversationQueue = new ConversationQueue(this);
- notificationProvider = NotificationProvider.getInstance(this);
- messageBuilder = new MessageBuilder(this, new MessageFactory(new DataStructureFactory()));
- liveWorkoutHandler = new LiveWorkoutHandler(this);
- incomingMessageHandlerFactory = IncomingMessageHandlerFactory.getInstance(this);
- addSupportedService(WithingsUUID.WITHINGS_SERVICE_UUID);
- addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS);
- addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE);
- addANCSService();
- activityUser = new ActivityUser();
-
- IntentFilter commandFilter = new IntentFilter(HANDS_CALIBRATION_CMD);
- commandFilter.addAction(START_HANDS_CALIBRATION_CMD);
- commandFilter.addAction(STOP_HANDS_CALIBRATION_CMD);
- final BroadcastReceiver commandReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (intent.getAction() == null) {
- return;
- }
-
- switch (intent.getAction()) {
- case HANDS_CALIBRATION_CMD:
- MoveHand moveHand = new MoveHand();
- moveHand.setHand(intent.getShortExtra("hand", (short) 1));
- moveHand.setMovement(intent.getShortExtra("movementAmount", (short) 1));
- sendToDevice(new WithingsMessage(WithingsMessageType.MOVE_HAND, moveHand));
- break;
- case START_HANDS_CALIBRATION_CMD:
- sendToDevice(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION));
- break;
- case STOP_HANDS_CALIBRATION_CMD:
- sendToDevice(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION));
- break;
- }
- }
- };
-
- LocalBroadcastManager.getInstance(GBApplication.getContext()).registerReceiver(commandReceiver, commandFilter);
+ @Override
+ protected WithingsUUIDs getWithingsUUIDs() {
+ return WithingsUUIDs.STEEL_HR;
}
@Override
- public void dispose() {
- synchronized (ConnectionMonitor) {
- backgroundTasksHandler.removeCallbacksAndMessages(null);
-
- super.dispose();
- }
- }
-
- @Override
- protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
- logger.debug("Starting initialization...");
- conversationQueue.clear();
- builder.setDeviceState(GBDevice.State.INITIALIZING);
- getDevice().setFirmwareVersion("N/A");
- getDevice().setFirmwareVersion2("N/A");
-
- // Delay initialization with 2 seconds to give the watch time to settle
- backgroundTasksHandler.removeCallbacksAndMessages(null);
- backgroundTasksHandler.postDelayed(this::postConnectInitialization, 2000);
-
- return builder;
- }
-
- private void postConnectInitialization() {
- final TransactionBuilder builder = createTransactionBuilder("delayed initialization");
- builder.notify(WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID, true);
- builder.requestMtu(512);
- builder.queue();
- }
-
- @Override
- public boolean connectFirstTime() {
- firstTimeConnect = true;
- return connect();
- }
-
- @Override
- public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
- super.onMtuChanged(gatt, mtu, status);
- if (status != BluetoothGatt.GATT_SUCCESS) {
- logger.error("Failed to change mtu - disconnecting");
- disconnect();
- return;
- }
-
- logger.debug("MTU has changed to {}", mtu);
- if (firstTimeConnect) {
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, getLocale()));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.START_HANDS_CALIBRATION));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.STOP_HANDS_CALIBRATION));
- 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.GET_ANCS_STATUS));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true)));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
- addScreenListCommands();
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SETUP_FINISHED), new SetupFinishedHandler(this));
- } else {
- Message message = new WithingsMessage(WithingsMessageType.PROBE);
- message.addDataStructure(new Probe((short) 1, (short) 1, 5100401));
- message.addDataStructure(new ProbeOsVersion((short) Build.VERSION.SDK_INT));
- conversationQueue.clear();
- addSimpleConversationToQueue(message, new AuthenticationHandler(this));
- }
-
- if (!firstTimeConnect) {
- finishInitialization();
- }
- conversationQueue.send();
- }
-
- public void doSync() {
- activitySampleHandler = new ActivitySampleHandler(this);
- conversationQueue.clear();
- try {
- if (syncInProgress) {
- return;
- }
-
- getDevice().setBusyTask(R.string.busy_task_syncing, getContext());
- syncInProgress = true;
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time()));
-
- if (shoudSync()) {
- logger.debug("Doing full sync...");
- WithingsMessage message = new WithingsMessage(WithingsMessageType.SET_USER);
- message.addDataStructure(getUser());
- // The UserSecret appears in the original communication with the HealthMate app. Until now GB works without the secret.
- // 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_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));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM_ENABLED));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_WORKOUT_SCREEN_LIST), new WorkoutScreenListHandler(this));
- Calendar c = Calendar.getInstance();
- c.setTimeInMillis(getLastSyncTimestamp());
- message = new WithingsMessage(WithingsMessageType.GET_ACTIVITY_SAMPLES, ExpectedResponse.EOT);
- message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
- addSimpleConversationToQueue(message, activitySampleHandler);
- message = new WithingsMessage(WithingsMessageType.GET_MOVEMENT_SAMPLES, ExpectedResponse.EOT);
- message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
- message.addDataStructure(new TypeVersion());
- addSimpleConversationToQueue(message, activitySampleHandler);
- message = new WithingsMessage(WithingsMessageType.GET_HEARTRATE_SAMPLES, ExpectedResponse.EOT);
- message.addDataStructure(new GetActivitySamples(c.getTimeInMillis() / 1000, (short) 0));
- message.addDataStructure(new TypeVersion());
- addSimpleConversationToQueue(message, activitySampleHandler);
- }
- } catch (Exception e) {
- logger.error("Could not synchronize! ", e);
- conversationQueue.clear();
- } finally {
- // This must be done in all cases or the watch won't respond anymore!
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SYNC_OK), new SyncFinishedHandler(this));
- }
- conversationQueue.send();
- }
-
-
- @Override
- public boolean onCharacteristicChanged(BluetoothGatt gatt,
- BluetoothGattCharacteristic characteristic,
- byte[] data) {
- if (super.onCharacteristicChanged(gatt, characteristic, data)) {
- return true;
- }
-
- boolean complete = messageBuilder.buildMessage(data);
- if (complete) {
- Message message = messageBuilder.getMessage();
- if (message.isIncomingMessage()) {
- logger.debug("received incoming message: {}", message.getType());
- IncomingMessageHandler handler = incomingMessageHandlerFactory.getHandler(message);
- handler.handleMessage(message);
- } else {
- conversationQueue.processResponse(message);
- }
- }
-
- return true;
- }
-
- @Override
- public void onSetCallState(CallSpec callSpec) {
- if (callSpec.command == CallSpec.CALL_INCOMING) {
- NotificationSpec notificationSpec = new NotificationSpec();
- notificationSpec.sourceAppId = "incoming.call";
- notificationSpec.title = callSpec.number;
- notificationSpec.sender = callSpec.name;
- notificationSpec.type = NotificationType.GENERIC_PHONE;
- notificationProvider.notifyClient(notificationSpec);
- } else {
- logger.info("Received yet unhandled call command: " + callSpec.command);
- }
- }
-
- @Override
- public void onNotification(NotificationSpec notificationSpec) {
- notificationProvider.notifyClient(notificationSpec);
- }
-
- @Override
- public void onSetAlarms(ArrayList extends Alarm> alarms) {
- if (alarms.size() > 3) {
- throw new IllegalArgumentException("Steel HR does only have three alarmslots!");
- }
-
- if (alarms.size() == 0) {
- return;
- }
-
- boolean noAlarmsEnabled = true;
- conversationQueue.clear();
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ALARM));
- for (Alarm alarm : alarms) {
- if (alarm.getEnabled() && !alarm.getUnused()) {
- noAlarmsEnabled = false;
- addAlarm(alarm);
- }
- }
-
- if (noAlarmsEnabled) {
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(false)));
- }
-
- conversationQueue.send();
- }
-
- @Override
- public boolean onDescriptorWriteRequest(BluetoothDevice device, int requestId, BluetoothGattDescriptor descriptor, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
- this.device = device;
- return true;
- }
-
- @Override
- public boolean onCharacteristicWriteRequest(BluetoothDevice device, int requestId, BluetoothGattCharacteristic characteristic, boolean preparedWrite, boolean responseNeeded, int offset, byte[] value) {
- if (characteristic.getUuid().equals(WithingsUUID.CONTROL_POINT_CHARACTERISTIC_UUID)) {
- logger.debug("Got GetNotificationAttributesRequest: " + GB.hexdump(value));
- GetNotificationAttributes request = new GetNotificationAttributes();
- request.deserialize(value);
- notificationProvider.handleNotificationAttributeRequest(request);
- }
-
- return true;
- }
-
- @Override
- public void onFetchRecordedData(int dataTypes) {
- doSync();
- }
-
- @Override
- public void onHeartRateTest() {
- conversationQueue.clear();
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_HR), new HeartRateHandler(this));
- conversationQueue.send();
- }
-
- @Override
- public void onSendConfiguration(String config) {
- try {
- switch (config) {
- case HuamiConst.PREF_WORKOUT_ACTIVITY_TYPES_SORTABLE:
- setWorkoutActivityTypes();
- break;
- case PREF_LANGUAGE:
- setLanguage();
- break;
- default:
- logger.debug("unknown configuration setting received: " + config);
- }
- } catch (Exception e) {
- GB.toast("Error setting configuration", Toast.LENGTH_LONG, GB.ERROR, e);
- }
- }
-
- @Override
- public void onTestNewFunction() {
- String hexMessage = "0105080015050900111006040102030507000000000000000000";
- conversationQueue.clear();
- addSimpleConversationToQueue(new SimpleHexToByteMessage(hexMessage));
- conversationQueue.send();
- }
-
- @Override
- public boolean useAutoConnect() {
- return false;
- }
-
- public void sendToDevice(Message message) {
- if (message == null) {
- return;
- }
-
- try {
- TransactionBuilder builder = createTransactionBuilder("conversation");
- builder.setCallback(this);
- BluetoothGattCharacteristic characteristic = getCharacteristic(WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID);
- if (characteristic == null) {
- logger.info("Characteristic with UUID " + WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID + " not found.");
- return;
- }
-
- byte[] rawData = message.getRawData();
- builder.writeChunkedData(characteristic, rawData, getMTU() - 3);
- builder.queue();
- } catch (Exception e) {
- logger.warn("Could not send message because of " + e.getMessage());
- }
- }
-
- public void sendAncsNotificationSourceNotification(NotificationSource notificationSource) {
- try {
- ServerTransactionBuilder builder = performServer("notificationSourceNotification");
- byte[] data = notificationSource.serialize();
- builder.notifyCharacteristicChanged(device, notificationSourceCharacteristic, data);
- builder.queue(getQueue());
- } catch (IOException e) {
- logger.error("Could not send notification.", e);
- GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e);
- }
- }
-
- public void sendAncsDataSourceNotification(GetNotificationAttributesResponse response) {
- try {
- ServerTransactionBuilder builder = performServer("dataSourceNotification");
- byte[] data = response.serialize();
- builder.notifyCharacteristicChanged(device, dataSourceCharacteristic, data);
- builder.queue(getQueue());
- } catch (IOException e) {
- logger.error("Could not send notification.", e);
- GB.toast("Could not send notification.", Toast.LENGTH_LONG, GB.ERROR, e);
- }
- }
-
- public void finishInitialization() {
- TransactionBuilder builder = createTransactionBuilder("setupFinished");
- builder.setDeviceState(GBDevice.State.INITIALIZED);
- builder.queue();
- logger.debug("Finished initialization.");
- }
-
- public void finishSync() {
- syncInProgress = false;
- if (getDevice().isBusy()) {
- getDevice().unsetBusyTask();
- getDevice().sendDeviceUpdateIntent(getContext());
- }
- activitySampleHandler.onSyncFinished();
- saveLastSyncTimestamp(new Date().getTime());
- }
-
- void onAuthenticationFinished() {
- if (!firstTimeConnect) {
- addScreenListCommands();
- doSync();
- } else {
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ANCS_STATUS, new AncsStatus(true)));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_ANCS_STATUS));
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.GET_BATTERY_STATUS), new BatteryStateHandler(this));
- conversationQueue.send();
- }
- }
-
- private void addAlarm(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);
- }
-
- Message alarmMessage = new WithingsMessage(WithingsMessageType.SET_ALARM, alarmSettings);
- if (!StringUtils.isEmpty(alarm.getTitle())) {
- AlarmName alarmName = new AlarmName(alarm.getTitle());
- alarmMessage.addDataStructure(alarmName);
- }
-
- addSimpleConversationToQueue(alarmMessage);
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_ALARM_ENABLED, new AlarmStatus(true)));
- }
-
- private short mapRepetitionToWithingsValue(Alarm alarm) {
- int repetition = 0;
- if (alarm.getRepetition(Alarm.ALARM_MON)) {
- repetition += 0x02;
- }
- if (alarm.getRepetition(Alarm.ALARM_TUE)) {
- repetition += 0x04;
- }
- if (alarm.getRepetition(Alarm.ALARM_WED)) {
- repetition += 0x08;
- }
- if (alarm.getRepetition(Alarm.ALARM_THU)) {
- repetition += 0x10;
- }
- if (alarm.getRepetition(Alarm.ALARM_FRI)) {
- repetition += 0x20;
- }
- if (alarm.getRepetition(Alarm.ALARM_SAT)) {
- repetition += 0x40;
- }
- if (alarm.getRepetition(Alarm.ALARM_SUN)) {
- repetition += 0x01;
- }
-
- return (short)(repetition + 0x80);
- }
-
- private void addANCSService() {
- BluetoothGattService withingsGATTService = new BluetoothGattService(WithingsUUID.WITHINGS_ANCS_SERVICE_UUID, BluetoothGattService.SERVICE_TYPE_PRIMARY);
- notificationSourceCharacteristic = new BluetoothGattCharacteristic(WithingsUUID.NOTIFICATION_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
- notificationSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(WithingsUUID.CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE));
- withingsGATTService.addCharacteristic(notificationSourceCharacteristic);
- withingsGATTService.addCharacteristic(new BluetoothGattCharacteristic(WithingsUUID.CONTROL_POINT_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_WRITE, BluetoothGattCharacteristic.PERMISSION_WRITE));
- dataSourceCharacteristic = new BluetoothGattCharacteristic(WithingsUUID.DATA_SOURCE_CHARACTERISTIC_UUID, BluetoothGattCharacteristic.PROPERTY_NOTIFY, BluetoothGattCharacteristic.PERMISSION_READ);
- dataSourceCharacteristic.addDescriptor(new BluetoothGattDescriptor(WithingsUUID.CCC_DESCRIPTOR_UUID, BluetoothGattCharacteristic.PERMISSION_WRITE));
- withingsGATTService.addCharacteristic(dataSourceCharacteristic);
- addSupportedServerService(withingsGATTService);
- }
-
- private void addSimpleConversationToQueue(Message message) {
- addSimpleConversationToQueue(message, null);
- }
-
- private void addSimpleConversationToQueue(Message message, ResponseHandler handler) {
- Conversation conversation = new SimpleConversation(handler);
- conversation.setRequest(message);
- conversationQueue.addConversation(conversation);
- }
-
- private void saveLastSyncTimestamp(@NonNull long timestamp) {
- SharedPreferences.Editor editor = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()).edit();
- editor.putLong(LAST_ACTIVITY_SYNC, timestamp);
- editor.apply();
- }
-
- private long getLastSyncTimestamp() {
- SharedPreferences settings = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
- long lastSyncTime = settings.getLong(LAST_ACTIVITY_SYNC, 0);
- if (lastSyncTime > 0) {
- return lastSyncTime;
- } else {
- Date currentDate = new Date();
- Calendar c = Calendar.getInstance();
- c.setTimeInMillis(currentDate.getTime());
- c.add(Calendar.HOUR, - 10);
- return c.getTimeInMillis();
- }
- }
-
- private boolean shoudSync() {
- long lastSynced = getLastSyncTimestamp();
- int minuteInMillis = 60 * 1000;
- return new Date().getTime() - lastSynced > minuteInMillis;
- }
-
- private User getUser() {
- User user = new User();
- ActivityUser activityUser = new ActivityUser();
- user.setName(activityUser.getName());
- user.setGender((byte) activityUser.getGender());
- user.setHeight(activityUser.getHeightCm());
- user.setWeight(activityUser.getWeightKg());
- user.setBirthdate(activityUser.getUserBirthday());
- return user;
- }
-
- private void addScreenListCommands() {
- // TODO: this needs to be more reworked, at the moment for example the notification screen is always on and this is full of magic numbers that need to be identified properly:
- Message message = new WithingsMessage(WithingsMessageType.SET_SCREEN_LIST);
- ScreenSettings settings = new ScreenSettings();
- settings.setId(0xff);
- settings.setIdOnDevice((byte)6);
- message.addDataStructure(settings);
-
- settings = new ScreenSettings();
- settings.setId(0x3d);
- settings.setIdOnDevice((byte)1);
- message.addDataStructure(settings);
-
- settings = new ScreenSettings();
- settings.setId(0x33);
- settings.setIdOnDevice((byte)4);
- message.addDataStructure(settings);
-
- settings = new ScreenSettings();
- settings.setId(0x2d);
- settings.setIdOnDevice((byte)2);
- message.addDataStructure(settings);
-
- settings = new ScreenSettings();
- settings.setId(0x2a);
- settings.setIdOnDevice((byte)3);
- message.addDataStructure(settings);
-
- settings = new ScreenSettings();
- settings.setId(0x26);
- settings.setIdOnDevice((byte)7);
- message.addDataStructure(settings);
-
- settings = new ScreenSettings();
- settings.setId(0x39);
- settings.setIdOnDevice((byte)9);
- message.addDataStructure(settings);
-
- message.addDataStructure(new EndOfTransmission());
- addSimpleConversationToQueue(message);
- }
-
- private void setWorkoutActivityTypes() {
- final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress());
-
- final List allActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_values));
- final List defaultActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_default));
- final String activityTypesPref = prefs.getString("workout_activity_types_sortable", null);
-
- final List enabledActivityTypes;
- if (activityTypesPref == null || activityTypesPref.equals("")) {
- enabledActivityTypes = defaultActivityTypes;
- } else {
- enabledActivityTypes = Arrays.asList(activityTypesPref.split(","));
- }
-
- conversationQueue.clear();
- for (int i = 0; i < enabledActivityTypes.size(); i++) {
- String workoutType = enabledActivityTypes.get(i);
- try {
- Message message = createWorkoutScreenMessage(workoutType);
- if (i == enabledActivityTypes.size() - 1) {
- message.addDataStructure(new EndOfTransmission());
- }
- addSimpleConversationToQueue(message);
- } catch (Exception e) {
- logger.warn("exception in setWorkoutActivityTypes", e);
- }
- }
-
- conversationQueue.send();
- }
-
- @NonNull
- private Message createWorkoutScreenMessage(String workoutType) {
- WithingsActivityType withingsActivityType = WithingsActivityType.fromPrefValue(workoutType);
- int code = withingsActivityType.getCode();
- Message message = new WithingsMessage(WithingsMessageType.SET_WORKOUT_SCREEN, ExpectedResponse.NONE);
- WorkoutScreen workoutScreen = new WorkoutScreen();
- workoutScreen.setId(code);
- final int stringId = getContext().getResources().getIdentifier("activity_type_" + workoutType, "string", getContext().getPackageName());
- workoutScreen.setName(getContext().getString(stringId));
- message.addDataStructure(workoutScreen);
-
- ImageMetaData imageMetaData = new ImageMetaData();
- imageMetaData.setHeight((byte)24);
- imageMetaData.setWidth((byte)22);
- message.addDataStructure(imageMetaData);
-
- ImageData imageData = new ImageData();
- final int drawableId = withingsActivityType.toActivityKind().getIcon();
- Drawable drawable = getContext().getDrawable(drawableId);
- imageData.setImageData(IconHelper.getIconBytesFromDrawable(drawable));
- message.addDataStructure(imageData);
-
- return message;
- }
-
- protected void setLanguage() {
- Locale locale = getLocale();
-
- conversationQueue.clear();
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, locale));
- conversationQueue.send();
- }
-
- private Locale getLocale() {
- String localeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())
- .getString(PREF_LANGUAGE, PREF_LANGUAGE_AUTO);
-
- if (localeString.equals(PREF_LANGUAGE_AUTO)) {
- localeString = java.util.Locale.getDefault().getLanguage();
- }
-
- String language = localeString.substring(0, 2);
- switch (language) {
- case "de":
- case "en":
- case "es":
- case "fr":
- case "it":
- return new Locale(language);
- default:
- return new Locale("en");
- }
- }
-
- private short getTimeMode() {
- if ("24h".equals(getDevicePrefs().getTimeFormat())) {
- return UserUnitConstants.UNIT_24H;
- } else {
- return UserUnitConstants.UNIT_12H;
- }
- }
-
- private short getUnit() {
- final DistanceUnit distanceUnit = GBApplication.getPrefs().getDistanceUnit();
-
- if (distanceUnit == DistanceUnit.METRIC) {
- return UserUnitConstants.UNIT_KM;
- } else {
- return UserUnitConstants.UNIT_MILES;
- }
- }
-
- @Override
- public boolean getImplicitCallbackModify() {
- return true;
- }
-
- @Override
- public void onSetTime() {
- conversationQueue.clear();
- addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_TIME, new Time()));
- conversationQueue.send();
+ public AbstractSampleProvider extends AbstractWithingsActivitySample> createSampleProvider(GBDevice device, DaoSession session) {
+ return new WithingsSteelHRSampleProvider(device, session);
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java
index 8bde32bbf0..d2dd144986 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java
@@ -18,36 +18,38 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.act
import java.util.List;
-import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractWithingsActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
/**
- * This class is needed for sleep tracking as the withings steel HR sends heartrate while sleeping in an extra activity.
- * This leads to breaking the sleep session in the sleep calculation of GB.
+ * This class is needed for sleep tracking as the Withings Steel HR sends heartrate while sleeping
+ * in an extra activity. This leads to breaking the sleep session in the sleep calculation of GB.
*/
public class SleepActivitySampleHelper {
- public static WithingsSteelHRActivitySample mergeIfNecessary(WithingsSteelHRSampleProvider provider, WithingsSteelHRActivitySample sample) {
+ public static T mergeIfNecessary(
+ AbstractSampleProvider provider, T sample) {
if (!shouldMerge(sample)) {
return sample;
}
- WithingsSteelHRActivitySample overlappingSample = getOverlappingSample(provider, (int)sample.getTimestamp());
+ T overlappingSample = getOverlappingSample(provider, (int) sample.getTimestamp());
if (overlappingSample != null) {
- sample = doMerge(overlappingSample, sample);
+ sample = doMerge(provider, overlappingSample, sample);
}
return sample;
}
- private static WithingsSteelHRActivitySample getOverlappingSample(WithingsSteelHRSampleProvider provider, long timestamp) {
- List samples = provider.getActivitySamples((int)timestamp - 500, (int)timestamp);
+ private static T getOverlappingSample(
+ AbstractSampleProvider provider, long timestamp) {
+ List samples = provider.getActivitySamples((int) timestamp - 500, (int) timestamp);
if (samples.isEmpty()) {
return null;
}
- for (int i = samples.size()-1; i >= 0; i--) {
- WithingsSteelHRActivitySample lastSample = samples.get(i);
+ for (int i = samples.size() - 1; i >= 0; i--) {
+ T lastSample = samples.get(i);
if (isNotHeartRateOnly(lastSample)) {
return lastSample;
}
@@ -56,11 +58,11 @@ public class SleepActivitySampleHelper {
return null;
}
- private static boolean isNotHeartRateOnly(WithingsSteelHRActivitySample lastSample) {
- return lastSample.getRawKind() != ActivityKind.NOT_MEASURED.getCode(); // && lastSample.getTimestamp() <= timestamp && (lastSample.getTimestamp() + lastSample.getDuration()) >= timestamp);
+ private static boolean isNotHeartRateOnly(AbstractWithingsActivitySample lastSample) {
+ return lastSample.getRawKind() != ActivityKind.NOT_MEASURED.getCode();
}
- private static boolean shouldMerge(WithingsSteelHRActivitySample sample) {
+ private static boolean shouldMerge(AbstractWithingsActivitySample sample) {
return sample.getSteps() == 0
&& sample.getDistance() == 0
&& sample.getRawKind() == -1
@@ -69,15 +71,14 @@ public class SleepActivitySampleHelper {
&& sample.getRawIntensity() == 0;
}
- private static WithingsSteelHRActivitySample doMerge(WithingsSteelHRActivitySample origin, WithingsSteelHRActivitySample update) {
- WithingsSteelHRActivitySample mergeResult = new WithingsSteelHRActivitySample();
+ private static T doMerge(
+ AbstractSampleProvider provider, T origin, T update) {
+ T mergeResult = provider.createActivitySample();
mergeResult.setTimestamp(update.getTimestamp());
mergeResult.setRawKind(origin.getRawKind());
mergeResult.setRawIntensity(origin.getRawIntensity());
mergeResult.setDuration(origin.getDuration() - (update.getTimestamp() - origin.getTimestamp()));
- mergeResult.setDevice(origin.getDevice());
mergeResult.setDeviceId(origin.getDeviceId());
- mergeResult.setUser(origin.getUser());
mergeResult.setUserId(origin.getUserId());
mergeResult.setProvider(origin.getProvider());
mergeResult.setHeartRate(update.getHeartRate());
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java
index 3ea2eb0bb8..0a6ac4a398 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java
@@ -18,6 +18,9 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.act
import java.util.Locale;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiWorkoutScreenActivityType;
@@ -61,6 +64,8 @@ public enum WithingsActivityType {
RIDING(26),
OTHER(36);
+ private static final Logger LOG = LoggerFactory.getLogger(WithingsActivityType.class);
+
private final int code;
WithingsActivityType(int typeCode) {
@@ -73,7 +78,8 @@ public enum WithingsActivityType {
return type;
}
}
- throw new RuntimeException("No matching WithingsActivityType for code: " + withingsCode);
+ LOG.warn("No matching WithingsActivityType for code: {}, falling back to OTHER", withingsCode);
+ return OTHER;
}
public int getCode() {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUIDs.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUIDs.java
new file mode 100644
index 0000000000..7832a20dcd
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUIDs.java
@@ -0,0 +1,54 @@
+/* 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication;
+
+import java.util.UUID;
+
+/**
+ * Holds the device-specific BLE UUIDs for Withings devices.
+ * Steel HR uses suffix "0037", Scanwatch uses suffix "005d".
+ */
+public class WithingsUUIDs {
+
+ public final UUID WITHINGS_SERVICE_UUID;
+ public final UUID WITHINGS_WRITE_CHARACTERISTIC_UUID;
+ public final UUID WITHINGS_APP_CHARACTERISTIC_UUID;
+ public final UUID WITHINGS_APP_CHARACTERISTIC2_UUID;
+ public final UUID CCC_DESCRIPTOR_UUID;
+ public final UUID WITHINGS_ANCS_SERVICE_UUID;
+ public final UUID NOTIFICATION_SOURCE_CHARACTERISTIC_UUID;
+ public final UUID CONTROL_POINT_CHARACTERISTIC_UUID;
+ public final UUID DATA_SOURCE_CHARACTERISTIC_UUID;
+
+ public WithingsUUIDs(final String suffix) {
+ WITHINGS_SERVICE_UUID = UUID.fromString("00000020-5749-5448-" + suffix + "-000000000000");
+ WITHINGS_WRITE_CHARACTERISTIC_UUID = UUID.fromString("00000024-5749-5448-" + suffix + "-000000000000");
+ WITHINGS_APP_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-" + suffix + "-000000000000");
+ WITHINGS_APP_CHARACTERISTIC2_UUID = UUID.fromString("10000028-5749-5448-" + suffix + "-000000000000");
+ CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
+ WITHINGS_ANCS_SERVICE_UUID = UUID.fromString("10000057-5749-5448-" + suffix + "-000000000000");
+ NOTIFICATION_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-" + suffix + "-000000000000");
+ CONTROL_POINT_CHARACTERISTIC_UUID = UUID.fromString("10000058-5749-5448-" + suffix + "-000000000000");
+ DATA_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("1000005a-5749-5448-" + suffix + "-000000000000");
+ }
+
+ /** Steel HR UUID set */
+ public static final WithingsUUIDs STEEL_HR = new WithingsUUIDs("0037");
+
+ /** Scanwatch UUID set */
+ public static final WithingsUUIDs SCANWATCH = new WithingsUUIDs("005d");
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java
index 107bbafb75..d26d525983 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java
@@ -17,13 +17,13 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
public abstract class AbstractResponseHandler implements ResponseHandler {
protected GBDevice device;
- protected WithingsSteelHRDeviceSupport support;
+ protected WithingsBaseDeviceSupport support;
- public AbstractResponseHandler(WithingsSteelHRDeviceSupport support) {
+ public AbstractResponseHandler(WithingsBaseDeviceSupport support) {
this.support = support;
this.device = support.getDevice();
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java
index dbbe0ec44b..a41f5fa4e6 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java
@@ -25,10 +25,10 @@ import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
-import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractWithingsActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.ActivityEntry;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutType;
@@ -52,7 +52,7 @@ public class ActivitySampleHandler extends AbstractResponseHandler {
private List activityEntries = new ArrayList<>();
private List heartrateEntries = new ArrayList<>();
- public ActivitySampleHandler(WithingsSteelHRDeviceSupport support) {
+ public ActivitySampleHandler(WithingsBaseDeviceSupport support) {
super(support);
}
@@ -200,32 +200,49 @@ public class ActivitySampleHandler extends AbstractResponseHandler {
}
private void saveData() {
- List activitySamples = new ArrayList<>();
- for (ActivityEntry activityEntry : activityEntries) {
- convertToSampleAndAddToList(activitySamples, activityEntry);
- }
- for (ActivityEntry activityEntry : heartrateEntries) {
- convertToSampleAndAddToList(activitySamples, activityEntry);
- }
-
- writeToDB(activitySamples);
- }
-
- private void writeToDB(List activitySamples) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(device, dbHandler.getDaoSession()).getId();
- WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(device, dbHandler.getDaoSession());
- for (WithingsSteelHRActivitySample sample : activitySamples) {
- sample.setDeviceId(deviceId);
- sample.setUserId(userId);
+ AbstractSampleProvider extends AbstractWithingsActivitySample> provider =
+ support.createSampleProvider(device, dbHandler.getDaoSession());
+
+ List activitySamples = new ArrayList<>();
+ for (ActivityEntry entry : activityEntries) {
+ activitySamples.add(convertToSample(provider, entry, userId, deviceId));
}
- provider.addGBActivitySamples(activitySamples.toArray(new WithingsSteelHRActivitySample[0]));
+ for (ActivityEntry entry : heartrateEntries) {
+ activitySamples.add(convertToSample(provider, entry, userId, deviceId));
+ }
+
+ writeToDB(provider, activitySamples);
} catch (Exception ex) {
logger.warn("Error saving activity data: " + ex.getLocalizedMessage());
}
}
+ @SuppressWarnings("unchecked")
+ private void writeToDB(
+ AbstractSampleProvider provider, List activitySamples) {
+ provider.addGBActivitySamples((T[]) activitySamples.toArray(new AbstractWithingsActivitySample[0]));
+ }
+
+ private AbstractWithingsActivitySample convertToSample(
+ AbstractSampleProvider extends AbstractWithingsActivitySample> provider,
+ ActivityEntry activityEntry, long userId, long deviceId) {
+ AbstractWithingsActivitySample sample = provider.createActivitySample();
+ sample.setTimestamp(activityEntry.getTimestamp());
+ sample.setDuration(activityEntry.getDuration());
+ sample.setHeartRate(activityEntry.getHeartrate());
+ sample.setSteps(activityEntry.getSteps());
+ sample.setRawKind(activityEntry.getRawKind());
+ sample.setCalories(activityEntry.getCalories());
+ sample.setDistance(activityEntry.getDistance());
+ sample.setRawIntensity(activityEntry.getRawIntensity());
+ sample.setDeviceId(deviceId);
+ sample.setUserId(userId);
+ return sample;
+ }
+
private void mergeHeartrateSamplesIntoActivitySammples() {
for (ActivityEntry heartrateEntry : heartrateEntries) {
for (ActivityEntry activityEntry : activityEntries) {
@@ -253,17 +270,4 @@ public class ActivitySampleHandler extends AbstractResponseHandler {
heartRateEntry.setCalories(activityEntry.getCalories());
}
}
-
- private void convertToSampleAndAddToList(List activitySamples, ActivityEntry activityEntry) {
- WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample();
- sample.setTimestamp(activityEntry.getTimestamp());
- sample.setDuration(activityEntry.getDuration());
- sample.setHeartRate(activityEntry.getHeartrate());
- sample.setSteps(activityEntry.getSteps());
- sample.setRawKind(activityEntry.getRawKind());
- sample.setCalories(activityEntry.getCalories());
- sample.setDistance(activityEntry.getDistance());
- sample.setRawIntensity(activityEntry.getRawIntensity());
- activitySamples.add(sample);
- }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java
index ad7bc5d5bd..9660f446c7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java
@@ -19,13 +19,13 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.com
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.BatteryValues;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public class BatteryStateHandler extends AbstractResponseHandler {
- public BatteryStateHandler(WithingsSteelHRDeviceSupport support) {
+ public BatteryStateHandler(WithingsBaseDeviceSupport support) {
super(support);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java
index b26da83d62..84de19352c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java
@@ -21,16 +21,16 @@ import org.slf4j.LoggerFactory;
import java.util.LinkedList;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public class ConversationQueue implements ConversationObserver
{
private static final Logger logger = LoggerFactory.getLogger(ConversationQueue.class);
private final LinkedList queue = new LinkedList<>();
- private WithingsSteelHRDeviceSupport support;
+ private WithingsBaseDeviceSupport support;
- public ConversationQueue(WithingsSteelHRDeviceSupport support) {
+ public ConversationQueue(WithingsBaseDeviceSupport support) {
this.support = support;
}
@@ -49,10 +49,12 @@ public class ConversationQueue implements ConversationObserver
if (!queue.isEmpty()) {
Conversation nextInLine = queue.peek();
if (nextInLine!= null) {
- logger.debug("Sending next queued message.");
+ logger.debug("Sending next queued message type={} (0x{})", nextInLine.getRequest().getType(), Integer.toHexString(nextInLine.getRequest().getType() & 0xffff));
Message request = nextInLine.getRequest();
support.sendToDevice(request);
}
+ } else {
+ logger.debug("Queue is empty, nothing to send.");
}
}
@@ -62,17 +64,28 @@ public class ConversationQueue implements ConversationObserver
}
if (conversation.getRequest().needsResponse() || conversation.getRequest().needsEOT()) {
+ logger.debug("addConversation: queuing type={} (0x{}) needsResponse={} needsEOT={}", conversation.getRequest().getType(), Integer.toHexString(conversation.getRequest().getType() & 0xffff), conversation.getRequest().needsResponse(), conversation.getRequest().needsEOT());
queue.add(conversation);
conversation.registerObserver(this);
} else {
+ logger.debug("addConversation: fire-and-forget type={} (0x{})", conversation.getRequest().getType(), Integer.toHexString(conversation.getRequest().getType() & 0xffff));
support.sendToDevice(conversation.getRequest());
}
}
public void processResponse(Message response) {
+ logger.debug("processResponse: type={} (0x{}), queue size={}", response.getType(), Integer.toHexString(response.getType() & 0xffff), queue.size());
+ for (Conversation c : queue) {
+ if (c.getRequest() != null) {
+ logger.debug(" queued conversation request type={} (0x{})", c.getRequest().getType(), Integer.toHexString(c.getRequest().getType() & 0xffff));
+ }
+ }
Conversation conversation = getConversation(response.getType());
if (conversation != null) {
+ logger.debug("processResponse: matched conversation for type={}", response.getType());
conversation.handleResponse(response);
+ } else {
+ logger.warn("processResponse: no conversation found for type={} (0x{}) -- message dropped!", response.getType(), Integer.toHexString(response.getType() & 0xffff));
}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/GetShortcutHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/GetShortcutHandler.java
new file mode 100644
index 0000000000..cf2c0aa651
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/GetShortcutHandler.java
@@ -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 . */
+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.ShortcutAction;
+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 GET_SHORTCUT} (cmd 0x0992).
+ *
+ * Reads the current long-press crown shortcut action from the watch and persists it to the
+ * device-specific {@link SharedPreferences} under
+ * {@link #PREF_SHORTCUT_ACTION} so that the UI can display the current value.
+ */
+public class GetShortcutHandler extends AbstractResponseHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(GetShortcutHandler.class);
+
+ /** Device-specific shared-prefs key for the shortcut action byte (stored as int). */
+ public static final String PREF_SHORTCUT_ACTION = "withings_scanwatch_shortcut_action";
+
+ public GetShortcutHandler(WithingsBaseDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ List data = response.getDataStructures();
+ if (data == null || data.isEmpty()) {
+ logger.warn("GetShortcutHandler: received empty response");
+ return;
+ }
+ for (WithingsStructure structure : data) {
+ if (structure instanceof ShortcutAction) {
+ final byte action = ((ShortcutAction) structure).getAction();
+ logger.info("GetShortcutHandler: current shortcut action = {}", action);
+ final SharedPreferences prefs =
+ GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
+ prefs.edit()
+ .putString(PREF_SHORTCUT_ACTION, String.valueOf(action))
+ .apply();
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java
index 55777eda71..c484ec9cca 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java
@@ -28,11 +28,11 @@ import java.util.GregorianCalendar;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
-import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
-import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
-import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
+import nodomain.freeyourgadget.gadgetbridge.entities.AbstractWithingsActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.SleepActivitySampleHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.HeartRate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveHeartRate;
@@ -42,7 +42,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.comm
public class HeartRateHandler extends AbstractResponseHandler {
private static final Logger logger = LoggerFactory.getLogger(HeartRateHandler.class);
- public HeartRateHandler(WithingsSteelHRDeviceSupport support) {
+ public HeartRateHandler(WithingsBaseDeviceSupport support) {
super(support);
}
@@ -56,9 +56,9 @@ public class HeartRateHandler extends AbstractResponseHandler {
private void handleHeartRateData(WithingsStructure structure) {
int heartRate = 0;
if (structure instanceof HeartRate) {
- heartRate = ((HeartRate)structure).getHeartrate();
+ heartRate = ((HeartRate) structure).getHeartrate();
} else if (structure instanceof LiveHeartRate) {
- heartRate = ((LiveHeartRate)structure).getHeartrate();
+ heartRate = ((LiveHeartRate) structure).getHeartrate();
}
if (heartRate > 0) {
@@ -66,21 +66,22 @@ public class HeartRateHandler extends AbstractResponseHandler {
}
}
- private void saveHeartRateData(int heartRate) {
- WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample();
- sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L));
- sample.setHeartRate(heartRate);
+ private void saveHeartRateData(int heartRate) {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(device, dbHandler.getDaoSession()).getId();
- WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(device, dbHandler.getDaoSession());
+ AbstractSampleProvider provider =
+ (AbstractSampleProvider) support.createSampleProvider(device, dbHandler.getDaoSession());
+ T sample = provider.createActivitySample();
+ sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L));
+ sample.setHeartRate(heartRate);
sample.setDeviceId(deviceId);
sample.setUserId(userId);
sample = SleepActivitySampleHelper.mergeIfNecessary(provider, sample);
provider.addGBActivitySample(sample);
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(GBDevice.EXTRA_DEVICE, support.getDevice())
- .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
+ .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, (java.io.Serializable) sample);
LocalBroadcastManager.getInstance(support.getContext()).sendBroadcast(intent);
} catch (Exception ex) {
logger.warn("Error saving current heart rate: " + ex.getLocalizedMessage());
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ScreenSettingsHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ScreenSettingsHandler.java
new file mode 100644
index 0000000000..5b88138452
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ScreenSettingsHandler.java
@@ -0,0 +1,55 @@
+/* Copyright (C) 2023-2024 Frank Ertl
+
+ 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ScreenSettings;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
+
+public class ScreenSettingsHandler extends AbstractResponseHandler {
+
+ private static final Logger logger = LoggerFactory.getLogger(ScreenSettingsHandler.class);
+
+ public ScreenSettingsHandler(WithingsBaseDeviceSupport support) {
+ super(support);
+ }
+
+ @Override
+ public void handleResponse(Message response) {
+ List data = response.getDataStructures();
+ if (data == null || data.isEmpty()) {
+ logger.warn("ScreenSettingsHandler: received empty response");
+ return;
+ }
+ logger.info("ScreenSettingsHandler: device reported {} screen(s)", data.size());
+ for (WithingsStructure structure : data) {
+ if (structure instanceof ScreenSettings) {
+ ScreenSettings screen = (ScreenSettings) structure;
+ logger.info("Screen from device: id=0x{} ({}), slot={}",
+ Integer.toHexString(screen.getId()),
+ screen.getId(),
+ screen.getIdOnDevice() & 0xFF);
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java
index 6d3f2aa99b..b11437c8cd 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java
@@ -16,13 +16,13 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class SetupFinishedHandler extends AbstractResponseHandler {
- public SetupFinishedHandler(WithingsSteelHRDeviceSupport support) {
+ public SetupFinishedHandler(WithingsBaseDeviceSupport support) {
super(support);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java
index 6c0bcf55ed..146574af90 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java
@@ -16,13 +16,13 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class SyncFinishedHandler extends AbstractResponseHandler {
- public SyncFinishedHandler(WithingsSteelHRDeviceSupport support) {
+ public SyncFinishedHandler(WithingsBaseDeviceSupport support) {
super(support);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java
index 56cc8a7477..e487a9d7fa 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java
@@ -25,7 +25,7 @@ import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreenList;
@@ -33,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.comm
public class WorkoutScreenListHandler extends AbstractResponseHandler {
- public WorkoutScreenListHandler(WithingsSteelHRDeviceSupport support) {
+ public WorkoutScreenListHandler(WithingsBaseDeviceSupport support) {
super(support);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java
index 251f8d1104..b1b618814a 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java
@@ -128,6 +128,9 @@ public class DataStructureFactory {
case WithingsStructureType.NOTIFICATION_APP_ID:
structure = new GlyphId();
break;
+ case WithingsStructureType.SHORTCUT_ACTION:
+ structure = new ShortcutAction();
+ break;
default:
structure = null;
logger.info("Received yet unknown structure type: " + structureTypeFromResponse);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java
index 6300379a58..1d1dd1fe21 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java
@@ -22,7 +22,6 @@ public class ScreenSettings extends WithingsStructure {
private int id;
- // TODO change to an actual unique ID. Must then be changed in User too.
private int userId = 123456;
private int yetUnknown1 = 0;
private int yetUnknown2 = 0;
@@ -37,6 +36,14 @@ public class ScreenSettings extends WithingsStructure {
this.id = id;
}
+ public int getUserId() {
+ return userId;
+ }
+
+ public void setUserId(int userId) {
+ this.userId = userId;
+ }
+
public byte getIdOnDevice() {
return idOnDevice;
}
@@ -50,6 +57,16 @@ public class ScreenSettings extends WithingsStructure {
return 22;
}
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
+ this.id = rawDataBuffer.getInt();
+ this.userId = rawDataBuffer.getInt();
+ this.yetUnknown1 = rawDataBuffer.getInt();
+ this.yetUnknown2 = rawDataBuffer.getInt();
+ this.idOnDevice = rawDataBuffer.get();
+ this.yetUnkown3 = rawDataBuffer.get();
+ }
+
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.putInt(this.id);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ShortcutAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ShortcutAction.java
new file mode 100644
index 0000000000..db1e5d896e
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ShortcutAction.java
@@ -0,0 +1,97 @@
+/* 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+import java.nio.ByteBuffer;
+
+/**
+ * Encodes the long-press crown shortcut action (TLV type 0x09A1).
+ *
+ * Wire format: 2-byte type (0x09A1) + 2-byte length (0x0001) + 1-byte action value.
+ *
+ *
Known action values:
+ *
+ * 0 = NONE
+ * 1 = ECG_MEAS
+ * 2 = SPO2_MEAS
+ * 3 = WORKOUT_START
+ * 4 = WORKOUT_SELECTION
+ * 5 = BREATH
+ * 6 = STOPWATCH
+ * 7 = TIMER
+ * 8 = DND
+ * 9 = QUICKLOOK
+ * 10 = FINDMYPHONE
+ * 11 = FLASHLIGHT
+ *
+ */
+public class ShortcutAction extends WithingsStructure {
+
+ // Action constants matching the Withings WPP protocol
+ public static final byte ACTION_NONE = 0;
+ // TODO: ACTION_ECG_MEAS and ACTION_SPO2_MEAS require the user to have accepted the health
+ // terms & conditions inside the official Withings app before the watch will honour
+ // them via BLE. Sending these values without prior T&C acceptance has no effect.
+ public static final byte ACTION_ECG_MEAS = 1;
+ public static final byte ACTION_SPO2_MEAS = 2;
+ public static final byte ACTION_WORKOUT_START = 3;
+ public static final byte ACTION_WORKOUT_SELECTION = 4;
+ public static final byte ACTION_BREATH = 5;
+ public static final byte ACTION_STOPWATCH = 6;
+ public static final byte ACTION_TIMER = 7;
+ public static final byte ACTION_DND = 8;
+ public static final byte ACTION_QUICKLOOK = 9;
+ public static final byte ACTION_FINDMYPHONE = 10;
+ public static final byte ACTION_FLASHLIGHT = 11;
+
+ private byte action;
+
+ /** No-arg constructor required by {@link DataStructureFactory}. */
+ public ShortcutAction() {}
+
+ public ShortcutAction(byte action) {
+ this.action = action;
+ }
+
+ /** @return the action byte received from the watch */
+ public byte getAction() {
+ return action;
+ }
+
+ @Override
+ public short getLength() {
+ // 4-byte TLV header + 1 byte payload
+ return 5;
+ }
+
+ @Override
+ protected void fillinTypeSpecificData(ByteBuffer buffer) {
+ buffer.put(action);
+ }
+
+ @Override
+ protected void fillFromRawDataAsBuffer(ByteBuffer buffer) {
+ if (buffer.remaining() >= 1) {
+ action = buffer.get();
+ }
+ }
+
+ @Override
+ public short getType() {
+ return WithingsStructureType.SHORTCUT_ACTION;
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsScreenId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsScreenId.java
new file mode 100644
index 0000000000..7afb170199
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsScreenId.java
@@ -0,0 +1,135 @@
+/* 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 . */
+package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
+
+/**
+ * Known screen IDs used in the Withings protocol SET_SCREEN_LIST command (message type 1292).
+ *
+ * Each constant represents the {@code id} field sent inside a {@link ScreenSettings} structure
+ * (bytes 2-3 of the 18-byte TYPE_SCREEN_LIST payload, i.e. the lower 16 bits of the 4-byte int).
+ * The {@code idOnDevice} field (slot/position) is a separate fixed value that determines the
+ * display order on the watch (ascending slot = earlier in the list).
+ *
+ *
ScanWatch screen IDs were confirmed from {@code reorder_screens.zip}: a BLE capture
+ * of the Withings HealthMate app sending CMD_SCREEN_LIST_SET (0x050C) with all 13 screens enabled
+ * in the order: data, sleep, ecg, elevation, heart rate, spo2, calories, settings, workouts,
+ * distance, steps, breathe, clock. Sorting the 13 TYPE_SCREEN_LIST entries by ascending slot
+ * number maps 1-to-1 onto that order, giving confirmed ID <-> name <-> slot bindings.
+ *
+ *
Steel HR screen IDs come from the original Gadgetbridge implementation and have not
+ * been reverified against a fresh packet capture.
+ */
+public final class WithingsScreenId {
+
+ private WithingsScreenId() {}
+
+ // -----------------------------------------------------------------------
+ // Screens - Withings Steel HR (source: original GB implementation)
+ // -----------------------------------------------------------------------
+
+ /** Notification screen. */
+ public static final int NOTIFICATIONS = 0xFF;
+
+ /** Heart rate (BPM) screen. */
+ public static final int HEART_RATE_STEEL = 0x3D;
+
+ /** Activity / step counter screen. */
+ public static final int ACTIVITY = 0x33;
+
+ /** Calories screen. */
+ public static final int CALORIES_STEEL = 0x2D;
+
+ /** Alarm/timer screen. */
+ public static final int ALARM = 0x2A;
+
+ /** Config / battery info screen. */
+ public static final int CONFIG = 0x26;
+
+ /** Chronometer / countdown screen. */
+ public static final int CHRONOMETER = 0x39;
+
+ // -----------------------------------------------------------------------
+ // Screens - Withings ScanWatch (confirmed from reorder_screens.zip capture)
+ // Fixed slot numbers are the watch's internal position values; display order
+ // follows ascending slot number.
+ // -----------------------------------------------------------------------
+
+ /** Date / activity data screen. Screen ID 0x007f, fixed slot 1. */
+ public static final int DATE = 0x007f;
+
+ /** Sleep screen. Screen ID 0x0080, fixed slot 2. */
+ public static final int SLEEP = 0x0080;
+
+ /** ECG screen. Screen ID 0x0081, fixed slot 3. */
+ public static final int ECG = 0x0081;
+
+ /** Elevation screen. Screen ID 0x0082, fixed slot 4. */
+ public static final int ELEVATION = 0x0082;
+
+ /** Heart rate screen. Screen ID 0x0084, fixed slot 6. */
+ public static final int HEART_RATE = 0x0084;
+
+ /** SpO2 / blood oxygen screen. Screen ID 0x008a, fixed slot 9. */
+ public static final int SPO2 = 0x008a;
+
+ /** Calories screen. Screen ID 0x008b, fixed slot 10. */
+ public static final int CALORIES = 0x008b;
+
+ /** Settings screen. Screen ID 0x0089, fixed slot 11. */
+ public static final int SETTINGS = 0x0089;
+
+ /** Workouts screen. Screen ID 0x008c, fixed slot 12. */
+ public static final int WORKOUTS = 0x008c;
+
+ /** Distance screen. Screen ID 0x0096, fixed slot 16. */
+ public static final int DISTANCE = 0x0096;
+
+ /** Steps screen. Screen ID 0x0097, fixed slot 17. */
+ public static final int STEPS = 0x0097;
+
+ /** Breathe / breathing exercises screen. Screen ID 0x00a1, fixed slot 18. */
+ public static final int BREATHE = 0x00a1;
+
+ /** Clock (alarms, stopwatch, timer) screen. Screen ID 0x0160, fixed slot 22. */
+ public static final int CLOCK = 0x0160;
+
+ /**
+ * Returns the fixed slot number for a given ScanWatch screen ID.
+ * Slot numbers are the watch's internal position values; display order follows ascending slot.
+ *
+ * @param screenId one of the ScanWatch screen ID constants in this class
+ * @return the fixed slot byte, or -1 if the screen ID is not a known ScanWatch screen
+ */
+ public static byte getScanwatchSlot(int screenId) {
+ switch (screenId) {
+ case DATE: return 1;
+ case SLEEP: return 2;
+ case ECG: return 3;
+ case ELEVATION: return 4;
+ case HEART_RATE: return 6;
+ case SPO2: return 9;
+ case CALORIES: return 10;
+ case SETTINGS: return 11;
+ case WORKOUTS: return 12;
+ case DISTANCE: return 16;
+ case STEPS: return 17;
+ case BREATHE: return 18;
+ case CLOCK: return 22;
+ default: return -1;
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java
index 6ae75c9254..e9022111c2 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java
@@ -68,5 +68,8 @@ public class WithingsStructureType {
public static final short WORKOUT_SCREEN_LIST = 316;
public static final short WORKOUT_SCREEN_DATA = 317;
+ /** Long-press crown shortcut action (type 0x09A1 = 2465). */
+ public static final short SHORTCUT_ACTION = (short) 0x09A1; // 2465
+
private WithingsStructureType() {}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java
index 01a6353a00..9356ae9ef7 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java
@@ -26,7 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.IconHelper;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.GlyphId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageMetaData;
@@ -35,9 +35,9 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GlyphRequestHandler implements IncomingMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(GlyphRequestHandler.class);
- private final WithingsSteelHRDeviceSupport support;
+ private final WithingsBaseDeviceSupport support;
- public GlyphRequestHandler(WithingsSteelHRDeviceSupport support) {
+ public GlyphRequestHandler(WithingsBaseDeviceSupport support) {
this.support = support;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java
index 53ca9ab196..294956d7c1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java
@@ -24,18 +24,18 @@ import java.io.IOException;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class MessageBuilder {
private static final Logger logger = LoggerFactory.getLogger(MessageBuilder.class);
- private WithingsSteelHRDeviceSupport support;
+ private WithingsBaseDeviceSupport support;
private MessageFactory messageFactory;
private ByteArrayOutputStream pendingMessage;
private Message message;
- public MessageBuilder(WithingsSteelHRDeviceSupport support, MessageFactory messageFactory) {
+ public MessageBuilder(WithingsBaseDeviceSupport support, MessageFactory messageFactory) {
this.support = support;
this.messageFactory = messageFactory;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java
index 8032812a2f..05da501693 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java
@@ -43,6 +43,13 @@ public class MessageFactory {
short messageTypeFromResponse = (short) (BLETypeConversions.toInt16(rawData[2], rawData[1]) & 16383);
short totalDataLength = (short) BLETypeConversions.toInt16(rawData[4], rawData[3]);
boolean isIncoming = rawData[1] == 65 || rawData[1] == -127;
+ logger.debug("createMessageFromRawData: rawData[1]=0x{} rawData[2]=0x{} -> type={} (0x{}) isIncoming={} dataLen={}",
+ Integer.toHexString(rawData[1] & 0xff),
+ Integer.toHexString(rawData[2] & 0xff),
+ messageTypeFromResponse,
+ Integer.toHexString(messageTypeFromResponse & 0xffff),
+ isIncoming,
+ totalDataLength);
Message message = new WithingsMessage(messageTypeFromResponse, isIncoming);
byte[] rawStructureData = Arrays.copyOfRange(rawData, 5, rawData.length);
List structures = dataStructureFactory.createStructuresFromRawData(rawStructureData);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java
index d45de5ccb8..79e0347942 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java
@@ -64,5 +64,10 @@ public final class WithingsMessageType {
public static final short GET_NOTIFICATION = 2404;
public static final short GET_UNICODE_GLYPH = 2403;
+ /** Get the currently configured long-press crown shortcut action (cmd 0x0992). */
+ public static final short GET_SHORTCUT = (short) 0x0992; // 2450
+ /** Set the long-press crown shortcut action (cmd 0x0989). */
+ public static final short SET_SHORTCUT = (short) 0x0989; // 2441
+
private WithingsMessageType() {}
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java
index e686721dab..ade213540c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java
@@ -22,7 +22,7 @@ import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.GlyphRequestHandler;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
@@ -31,14 +31,14 @@ public class IncomingMessageHandlerFactory {
private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandlerFactory.class);
private static IncomingMessageHandlerFactory instance;
- private final WithingsSteelHRDeviceSupport support;
+ private final WithingsBaseDeviceSupport support;
private Map handlers = new HashMap<>();
- private IncomingMessageHandlerFactory(WithingsSteelHRDeviceSupport support) {
+ private IncomingMessageHandlerFactory(WithingsBaseDeviceSupport support) {
this.support = support;
}
- public static IncomingMessageHandlerFactory getInstance(WithingsSteelHRDeviceSupport support) {
+ public static IncomingMessageHandlerFactory getInstance(WithingsBaseDeviceSupport support) {
if (instance == null) {
instance = new IncomingMessageHandlerFactory(support);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java
index f835d4cc20..584c6d6c74 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java
@@ -33,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsStee
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.SleepActivitySampleHelper;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveHeartRate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
@@ -41,9 +41,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.comm
public class LiveHeartrateHandler implements IncomingMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(LiveHeartrateHandler.class);
- private final WithingsSteelHRDeviceSupport support;
+ private final WithingsBaseDeviceSupport support;
- public LiveHeartrateHandler(WithingsSteelHRDeviceSupport support) {
+ public LiveHeartrateHandler(WithingsBaseDeviceSupport support) {
this.support = support;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java
index 8935e761e9..a3099f4821 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java
@@ -32,7 +32,7 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
import nodomain.freeyourgadget.gadgetbridge.externalevents.opentracks.OpenTracksController;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutEnd;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutPauseState;
@@ -48,10 +48,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class LiveWorkoutHandler implements IncomingMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(LiveWorkoutHandler.class);
- private final WithingsSteelHRDeviceSupport support;
+ private final WithingsBaseDeviceSupport support;
private BaseActivitySummary baseActivitySummary;
- public LiveWorkoutHandler(WithingsSteelHRDeviceSupport support) {
+ public LiveWorkoutHandler(WithingsBaseDeviceSupport support) {
this.support = support;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java
index 4297e498e7..0d103e6c1c 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java
@@ -29,7 +29,7 @@ import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.IconHelper;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
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.SourceAppId;
@@ -42,10 +42,10 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class NotificationRequestHandler implements IncomingMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(NotificationRequestHandler.class);
- private final WithingsSteelHRDeviceSupport support;
+ private final WithingsBaseDeviceSupport support;
private Map appIconCache = new HashMap<>();
- public NotificationRequestHandler(WithingsSteelHRDeviceSupport support) {
+ public NotificationRequestHandler(WithingsBaseDeviceSupport support) {
this.support = support;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java
index c3a85fe445..68707470c8 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java
@@ -16,7 +16,7 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.ExpectedResponse;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
@@ -24,9 +24,9 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.comm
public class SyncRequestHandler implements IncomingMessageHandler {
- private final WithingsSteelHRDeviceSupport support;
+ private final WithingsBaseDeviceSupport support;
- public SyncRequestHandler(WithingsSteelHRDeviceSupport support) {
+ public SyncRequestHandler(WithingsBaseDeviceSupport support) {
this.support = support;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java
index 89108d25bd..65c9928b4f 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java
@@ -25,17 +25,17 @@ import java.util.Map;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
-import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class NotificationProvider {
private static final Logger logger = LoggerFactory.getLogger(NotificationProvider.class);
- private final WithingsSteelHRDeviceSupport support;
+ private final WithingsBaseDeviceSupport support;
private final Map pendingNotifications = new HashMap<>();
private static NotificationProvider instance;
- public static NotificationProvider getInstance(WithingsSteelHRDeviceSupport support) {
+ public static NotificationProvider getInstance(WithingsBaseDeviceSupport support) {
if (instance == null) {
instance = new NotificationProvider(support);
}
@@ -43,7 +43,7 @@ public class NotificationProvider {
return instance;
}
- private NotificationProvider(WithingsSteelHRDeviceSupport support) {
+ private NotificationProvider(WithingsBaseDeviceSupport support) {
this.support = support;
}
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 7ebe8f9867..54b6e29127 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -1834,6 +1834,86 @@
- other
+
+
+ - @string/withings_scanwatch_screen_date
+ - @string/withings_scanwatch_screen_sleep
+ - @string/withings_scanwatch_screen_ecg
+ - @string/withings_scanwatch_screen_elevation
+ - @string/withings_scanwatch_screen_heart_rate
+ - @string/withings_scanwatch_screen_spo2
+ - @string/withings_scanwatch_screen_calories
+ - @string/withings_scanwatch_screen_settings
+ - @string/withings_scanwatch_screen_workouts
+ - @string/withings_scanwatch_screen_distance
+ - @string/withings_scanwatch_screen_steps
+ - @string/withings_scanwatch_screen_breathe
+ - @string/withings_scanwatch_screen_clock
+
+
+
+ - date
+ - sleep
+ - ecg
+ - elevation
+ - heart_rate
+ - spo2
+ - calories
+ - settings
+ - workouts
+ - distance
+ - steps
+ - breathe
+ - clock
+
+
+
+
+ - date
+ - sleep
+ - ecg
+ - elevation
+ - heart_rate
+ - spo2
+ - calories
+ - settings
+ - workouts
+ - distance
+ - steps
+ - breathe
+ - clock
+
+
+
+
+ - @string/withings_scanwatch_shortcut_none
+ - @string/withings_scanwatch_shortcut_ecg
+ - @string/withings_scanwatch_shortcut_spo2
+ - @string/withings_scanwatch_shortcut_workout_start
+ - @string/withings_scanwatch_shortcut_workout_selection
+ - @string/withings_scanwatch_shortcut_breathe
+ - @string/withings_scanwatch_shortcut_stopwatch
+ - @string/withings_scanwatch_shortcut_timer
+ - @string/withings_scanwatch_shortcut_dnd
+ - @string/withings_scanwatch_shortcut_quicklook
+
+
+
+ - 0
+ - 1
+ - 2
+ - 3
+ - 4
+ - 5
+ - 6
+ - 7
+ - 8
+ - 9
+
+
- @string/menuitem_pai
- @string/menuitem_dnd
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 8480784813..bbb712fbd9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -3486,6 +3486,7 @@
1MORE SonoFlow
Sony Wena 3
Withings Steel HR
+ Withings Scanwatch
Disabled
Media Play
Media Pause
@@ -3736,6 +3737,37 @@
Finally align the activity hand to 100%. Please be aware that this hand only moves clockwise.
Previous
Next
+ Watch screens
+ Choose which screens appear on the watch and drag to reorder them
+ Date
+ Sleep
+ ECG
+ Elevation
+ Heart rate
+ SpO2 / Blood oxygen
+ Calories
+ Settings
+ Workouts
+ Distance
+ Steps
+ Breathe
+ Clock
+
+
+ Long-press crown shortcut
+ Action launched by holding the crown button
+ None
+ ECG measurement
+ SpO2 / Blood oxygen measurement
+ Start workout
+ Workout selection
+ Breathe
+ Stopwatch
+ Timer
+ Do not disturb
+ Quick look
+ Find my phone
+ Flashlight
drag handle
FOUND IT
Trigger a full sync of all activity data
diff --git a/app/src/main/res/xml/devicesettings_withingsscanwatch.xml b/app/src/main/res/xml/devicesettings_withingsscanwatch.xml
new file mode 100644
index 0000000000..1e17b474f3
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_withingsscanwatch.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinatorTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinatorTest.java
index aa81327d61..7e9dc7ce83 100644
--- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinatorTest.java
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/AbstractDeviceCoordinatorTest.java
@@ -19,6 +19,7 @@ public class AbstractDeviceCoordinatorTest extends TestBase {
final Map bluetoothNameToExpectedType = new HashMap<>() {{
put("Active 2 NFC (Round)", DeviceType.AMAZFITACTIVE2NFC);
put("Amazfit Band 7", DeviceType.AMAZFITBAND7); // #2945
+ put("ScanWatch CA", DeviceType.WITHINGS_SCANWATCH);
put("Amazfit GTR 3 Pro", DeviceType.AMAZFITGTR3PRO); // #2442
put("Amazfit GTS", DeviceType.AMAZFITGTS); // #5391
put("Amazfit GTS 3", DeviceType.AMAZFITGTS3); // #2442