mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
feat(withings,scanwatch): Initial support Withings Scanwatch
Pairing Shortcut press-and-hold crown
This commit is contained in:
@@ -146,6 +146,7 @@ public class GBDaoGenerator {
|
||||
addPineTimeActivitySample(schema, user, device);
|
||||
addPolarH10ActivitySample(schema, user, device);
|
||||
addWithingsSteelHRActivitySample(schema, user, device);
|
||||
addWithingsScanwatchActivitySample(schema, user, device);
|
||||
addGenericBloodPressureSample(schema, user, device);
|
||||
addHybridHRActivitySample(schema, user, device);
|
||||
addHybridHRSpo2Sample(schema, user, device);
|
||||
@@ -1640,12 +1641,26 @@ public class GBDaoGenerator {
|
||||
private static Entity addWithingsSteelHRActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "WithingsSteelHRActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty("duration").notNull();
|
||||
addCommonActivitySampleProperties("AbstractWithingsActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty("duration").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("distance").notNull();
|
||||
activitySample.addIntProperty("calories").notNull();
|
||||
activitySample.addIntProperty("distance").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("calories").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addWithingsScanwatchActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "WithingsScanwatchActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
addCommonActivitySampleProperties("AbstractWithingsActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty("duration").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("distance").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("calories").notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
addHeartRateProperties(activitySample);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
return activitySample;
|
||||
|
||||
+1
-1
@@ -125,7 +125,7 @@ public class DevicesFragment extends Fragment {
|
||||
deviceListView.setAdapter(this.mGBDeviceAdapter);
|
||||
|
||||
// get activity data asynchronously, this fills the deviceActivityHashMap
|
||||
// and calls refreshPairedDevices() → notifyDataSetChanged
|
||||
// and calls refreshPairedDevices() -> notifyDataSetChanged
|
||||
deviceListView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
+4
-4
@@ -98,16 +98,16 @@ public class DashboardBloodPressureWidget extends AbstractGaugeWidget {
|
||||
final int color;
|
||||
final float gaugeValue;
|
||||
if (systolic < 120 && diastolic < 80) {
|
||||
color = android.graphics.Color.rgb(76, 175, 80); // green – Normal
|
||||
color = android.graphics.Color.rgb(76, 175, 80); // green - Normal
|
||||
gaugeValue = 0.25f;
|
||||
} else if (systolic < 130 && diastolic < 80) {
|
||||
color = android.graphics.Color.rgb(139, 195, 74); // lime – Elevated
|
||||
color = android.graphics.Color.rgb(139, 195, 74); // lime - Elevated
|
||||
gaugeValue = 0.45f;
|
||||
} else if (systolic < 140 || diastolic < 90) {
|
||||
color = android.graphics.Color.rgb(255, 152, 0); // orange – Stage 1
|
||||
color = android.graphics.Color.rgb(255, 152, 0); // orange - Stage 1
|
||||
gaugeValue = 0.65f;
|
||||
} else {
|
||||
color = android.graphics.Color.rgb(244, 67, 54); // red – Stage 2+
|
||||
color = android.graphics.Color.rgb(244, 67, 54); // red - Stage 2+
|
||||
gaugeValue = 0.88f;
|
||||
}
|
||||
drawSimpleGauge(color, gaugeValue);
|
||||
|
||||
+4
-4
@@ -40,7 +40,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.PaiSample;
|
||||
*
|
||||
* The gauge is rendered as a segmented arc mirroring the visual language used throughout
|
||||
* the rest of the dashboard. When the total PAI score meets or exceeds the device target,
|
||||
* the arc is drawn in full using two proportional color segments — chart_pai_weekly for
|
||||
* the arc is drawn in full using two proportional color segments - chart_pai_weekly for
|
||||
* the carry-over (total minus today) portion and chart_pai_today for today's contribution.
|
||||
* When the target has not yet been reached, the arc is only partially filled to reflect
|
||||
* the fraction of the goal completed.
|
||||
@@ -81,7 +81,7 @@ public class DashboardPaiWidget extends AbstractGaugeWidget {
|
||||
|
||||
// Use getAllSamples bounded by timeFrom and timeTo to ensure we only show data
|
||||
// that actually exists within the selected day. Using getLatestSample(timeTo) has no lower
|
||||
// bound and could bleed back to a previous day's sample — for example
|
||||
// bound and could bleed back to a previous day's sample - for example
|
||||
// if Sunday has no data or the device was reset, it would incorrectly return Saturday's score.
|
||||
final long windowStartMs = dashboardData.timeFrom * 1000L;
|
||||
final long windowEndMs = dashboardData.timeTo * 1000L;
|
||||
@@ -97,7 +97,7 @@ public class DashboardPaiWidget extends AbstractGaugeWidget {
|
||||
coordinator.getPaiSampleProvider(dev, dbHandler.getDaoSession());
|
||||
|
||||
if (provider == null) {
|
||||
LOG.warn("Device {} returned a null PAI sample provider — skipping", dev);
|
||||
LOG.warn("Device {} returned a null PAI sample provider - skipping", dev);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ public class DashboardPaiWidget extends AbstractGaugeWidget {
|
||||
final PaiData paiData = (PaiData) dashboardData.get(DATA_KEY);
|
||||
|
||||
if (paiData == null || paiData.target <= 0) {
|
||||
// No data available — render an empty gauge rather than crashing.
|
||||
// No data available - render an empty gauge rather than crashing.
|
||||
drawSimpleGauge(0, -1);
|
||||
setText("0");
|
||||
return;
|
||||
|
||||
+12
@@ -30,6 +30,18 @@ public class GadgetbridgeUpdate_129 implements DBUpdateScript {
|
||||
+ HuaweiWorkoutSummarySampleDao.Properties.RawGpsFileLocation.columnName + "\" TEXT";
|
||||
db.execSQL(statement);
|
||||
}
|
||||
db.execSQL("CREATE TABLE IF NOT EXISTS \"WITHINGS_SCANWATCH_ACTIVITY_SAMPLE\" (" +
|
||||
"\"TIMESTAMP\" INTEGER NOT NULL," +
|
||||
"\"DEVICE_ID\" INTEGER NOT NULL," +
|
||||
"\"USER_ID\" INTEGER NOT NULL," +
|
||||
"\"DURATION\" INTEGER NOT NULL DEFAULT -1," +
|
||||
"\"RAW_KIND\" INTEGER NOT NULL DEFAULT -1," +
|
||||
"\"STEPS\" INTEGER NOT NULL DEFAULT -1," +
|
||||
"\"DISTANCE\" INTEGER NOT NULL DEFAULT -1," +
|
||||
"\"CALORIES\" INTEGER NOT NULL DEFAULT -1," +
|
||||
"\"HEART_RATE\" INTEGER NOT NULL DEFAULT -1," +
|
||||
"\"RAW_INTENSITY\" INTEGER NOT NULL DEFAULT -1," +
|
||||
"PRIMARY KEY (\"TIMESTAMP\", \"DEVICE_ID\"));");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+6
-6
@@ -183,8 +183,8 @@ public class FitProConstants {
|
||||
|
||||
//00 01 00 96 03 08 16 7f
|
||||
//^^ zeros ^^ on/off ^^ sep ^^unknown ^^minutes ^^from ^^ to ^^ unknown
|
||||
//minutes are array of minutes by 15, in 45,60, 75...→ 3,4,5...
|
||||
//the byte 4 → 96 could be experimented with to set different values..., byte 3 is probably Hi, byte 4 Low
|
||||
//minutes are array of minutes by 15, in 45,60, 75...-> 3,4,5...
|
||||
//the byte 4 -> 96 could be experimented with to set different values..., byte 3 is probably Hi, byte 4 Low
|
||||
//public static final byte[] CMD_SET_LONG_SIT_REMINDER = new byte[]{(byte) 0x12, (byte) 0x5};
|
||||
//maybe could be usign the ON/OFF?
|
||||
//public static final byte[] VALUE_SET_LONG_SIT_REMINDER_ON = new byte[]{0x0, 0x1, 0x0, (byte) 0x96};
|
||||
@@ -194,7 +194,7 @@ public class FitProConstants {
|
||||
//Value: cd 00 0a 12 01 09 00 05 00 01 e0 05 28 OFF
|
||||
// 00 1 2 3 4 5 6 7 8 9 10 11 12
|
||||
//byte 8 on/off
|
||||
// 9,10,11,12 → time from/to
|
||||
// 9,10,11,12 -> time from/to
|
||||
//public static final byte[] CMD_SET_DISPLAY_ON_LIFT = new byte[]{(byte) 0x12, (byte) 0x9};
|
||||
//maybe could be usign the ON/OFF?
|
||||
//public static final byte VALUE_SET_DISPLAY_ON_LIFT_ON = 0x1;
|
||||
@@ -207,7 +207,7 @@ public class FitProConstants {
|
||||
// (byte) 0x12, (byte) 0x01, (byte) 0x0b, (byte) 0x00, (byte) 0x01, (byte) 0x01};
|
||||
|
||||
/*init procedure:
|
||||
get pair cd 00 06 12 01 0a 00 01 02 → 18, 10
|
||||
get pair cd 00 06 12 01 0a 00 01 02 -> 18, 10
|
||||
cd 00 09 12 01 01 00 04 55 f8 36 90
|
||||
cd 00 05 1a 01 0a 00 00
|
||||
cd 00 05 1a 01 0c 00 00
|
||||
@@ -218,8 +218,8 @@ public class FitProConstants {
|
||||
cd 00 05 1a 01 0f 00 00
|
||||
cd 00 05 1a 01 10 00 00
|
||||
dc 00 05 1a 01 00 1c 01
|
||||
something cd 00 05 20 01 02 00 00 → 32, 2
|
||||
real time step cd 00 06 15 01 06 00 01 01 → 21, 6
|
||||
something cd 00 05 20 01 02 00 00 -> 32, 2
|
||||
real time step cd 00 06 15 01 06 00 01 01 -> 21, 6
|
||||
*/
|
||||
|
||||
//received heartrate
|
||||
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.withingsscanwatch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLEDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsScanwatchActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingsscanwatch.WithingsScanwatchDeviceSupport;
|
||||
|
||||
public class WithingsScanwatchDeviceCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("(?i)^ScanWatch.*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<AbstractDao<?, ?>, Property> getAllDeviceDao(@NonNull final DaoSession session) {
|
||||
final Map<AbstractDao<?, ?>, Property> map = new HashMap<>(1);
|
||||
map.put(session.getWithingsScanwatchActivitySampleDao(), WithingsScanwatchActivitySampleDao.Properties.DeviceId);
|
||||
return map;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
|
||||
return new int[]{ R.xml.devicesettings_withingsscanwatch };
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
|
||||
return new WithingsScanwatchSettingsCustomizer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBondingStyle() {
|
||||
return BONDING_STYLE_BOND;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRemSleep(@NonNull GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsDataFetching(@NonNull final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking(@NonNull GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRecordedActivities(final GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new WithingsScanwatchSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAlarmSlotCount(GBDevice gbDevice) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmTitle(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmDescription(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device, int position) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Withings";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRealtimeData(@NonNull GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedLanguageSettings(GBDevice device) {
|
||||
return new String[]{ "auto", "de_DE", "en_US", "es_ES", "fr_FR", "it_IT" };
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<? extends DeviceSupport> getDeviceSupportClass(final GBDevice device) {
|
||||
return WithingsScanwatchDeviceSupport.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_withings_scanwatch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDefaultIconResource() {
|
||||
return R.drawable.ic_device_watchxplus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceKind getDeviceKind(@NonNull GBDevice device) {
|
||||
return DeviceKind.WATCH;
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.withingsscanwatch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsScanwatchActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsScanwatchActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class WithingsScanwatchSampleProvider extends AbstractSampleProvider<WithingsScanwatchActivitySample> {
|
||||
public WithingsScanwatchSampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<WithingsScanwatchActivitySample, ?> getSampleDao() {
|
||||
return getSession().getWithingsScanwatchActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return WithingsScanwatchActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return WithingsScanwatchActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return WithingsScanwatchActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActivityKind normalizeType(int rawType) {
|
||||
switch (rawType) {
|
||||
case 1: return ActivityKind.LIGHT_SLEEP;
|
||||
case 2: return ActivityKind.DEEP_SLEEP;
|
||||
default: return ActivityKind.fromCode(rawType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(ActivityKind activityKind) {
|
||||
switch (activityKind) {
|
||||
case UNKNOWN: return 0;
|
||||
case LIGHT_SLEEP: return 1;
|
||||
case DEEP_SLEEP: return 2;
|
||||
default: return activityKind.getCode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
if (rawIntensity > 0) {
|
||||
return (float) (Math.log(rawIntensity) / 8);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WithingsScanwatchActivitySample createActivitySample() {
|
||||
return new WithingsScanwatchActivitySample();
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.withingsscanwatch;
|
||||
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
|
||||
public class WithingsScanwatchSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
|
||||
|
||||
static final String PREF_SCREENS_SORTABLE = "withings_scanwatch_screens_sortable";
|
||||
static final String PREF_SHORTCUT_ACTION = "withings_scanwatch_shortcut_action";
|
||||
|
||||
@Override
|
||||
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) {
|
||||
handler.addPreferenceHandlerFor(PREF_SCREENS_SORTABLE);
|
||||
handler.addPreferenceHandlerFor(PREF_SHORTCUT_ACTION);
|
||||
|
||||
final ListPreference shortcutPref = handler.findPreference(PREF_SHORTCUT_ACTION);
|
||||
if (shortcutPref != null) {
|
||||
shortcutPref.setSummaryProvider(ListPreference.SimpleSummaryProvider.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getPreferenceKeysWithSummary() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
public static final Creator<WithingsScanwatchSettingsCustomizer> CREATOR = new Creator<WithingsScanwatchSettingsCustomizer>() {
|
||||
@Override
|
||||
public WithingsScanwatchSettingsCustomizer createFromParcel(final Parcel in) {
|
||||
return new WithingsScanwatchSettingsCustomizer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public WithingsScanwatchSettingsCustomizer[] newArray(final int size) {
|
||||
return new WithingsScanwatchSettingsCustomizer[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -33,7 +33,7 @@ import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.AbstractGBActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsBaseDeviceSupport;
|
||||
|
||||
public class WithingsCalibrationActivity extends AbstractGBActivity {
|
||||
|
||||
@@ -64,7 +64,7 @@ public class WithingsCalibrationActivity extends AbstractGBActivity {
|
||||
setContentView(R.layout.activity_withings_calibration);
|
||||
List<GBDevice> 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);
|
||||
|
||||
+33
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
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);
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
+193
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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.
|
||||
*
|
||||
* <p>Screen ID <-> slot mapping is defined in {@link WithingsScreenId#getScanwatchSlot(int)}.
|
||||
*
|
||||
* <p>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<String> defaultScreens = Arrays.asList(
|
||||
getContext().getResources().getStringArray(R.array.pref_withings_scanwatch_screens_default));
|
||||
|
||||
final String screensPref = prefs.getString(PREF_SCREENS_SORTABLE, null);
|
||||
final List<String> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -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;
|
||||
}
|
||||
|
||||
+817
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
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.
|
||||
*
|
||||
* <p>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<String> allActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_values));
|
||||
final List<String> 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<String> 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();
|
||||
}
|
||||
}
|
||||
+15
-752
@@ -16,763 +16,26 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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<String> allActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_values));
|
||||
final List<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
|
||||
+19
-18
@@ -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 extends AbstractWithingsActivitySample> T mergeIfNecessary(
|
||||
AbstractSampleProvider<T> 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<WithingsSteelHRActivitySample> samples = provider.getActivitySamples((int)timestamp - 500, (int)timestamp);
|
||||
private static <T extends AbstractWithingsActivitySample> T getOverlappingSample(
|
||||
AbstractSampleProvider<T> provider, long timestamp) {
|
||||
List<T> 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 extends AbstractWithingsActivitySample> T doMerge(
|
||||
AbstractSampleProvider<T> 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());
|
||||
|
||||
+7
-1
@@ -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() {
|
||||
|
||||
+54
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
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");
|
||||
}
|
||||
+3
-3
@@ -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();
|
||||
}
|
||||
|
||||
+38
-34
@@ -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<ActivityEntry> activityEntries = new ArrayList<>();
|
||||
private List<ActivityEntry> 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<WithingsSteelHRActivitySample> activitySamples = new ArrayList<>();
|
||||
for (ActivityEntry activityEntry : activityEntries) {
|
||||
convertToSampleAndAddToList(activitySamples, activityEntry);
|
||||
}
|
||||
for (ActivityEntry activityEntry : heartrateEntries) {
|
||||
convertToSampleAndAddToList(activitySamples, activityEntry);
|
||||
}
|
||||
|
||||
writeToDB(activitySamples);
|
||||
}
|
||||
|
||||
private void writeToDB(List<WithingsSteelHRActivitySample> 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<AbstractWithingsActivitySample> 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 <T extends AbstractWithingsActivitySample> void writeToDB(
|
||||
AbstractSampleProvider<T> provider, List<AbstractWithingsActivitySample> 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<WithingsSteelHRActivitySample> 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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -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<Conversation> 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/* Copyright (C) 2024 Gadgetbridge contributors
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.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).
|
||||
*
|
||||
* <p>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<WithingsStructure> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-13
@@ -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 <T extends AbstractWithingsActivitySample> 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<T> provider =
|
||||
(AbstractSampleProvider<T>) 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());
|
||||
|
||||
+55
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
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<WithingsStructure> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -16,13 +16,13 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -16,13 +16,13 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+18
-1
@@ -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);
|
||||
|
||||
+97
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* Encodes the long-press crown shortcut action (TLV type 0x09A1).
|
||||
*
|
||||
* <p>Wire format: 2-byte type (0x09A1) + 2-byte length (0x0001) + 1-byte action value.
|
||||
*
|
||||
* <p>Known action values:
|
||||
* <pre>
|
||||
* 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
|
||||
* </pre>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
+135
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
|
||||
|
||||
/**
|
||||
* Known screen IDs used in the Withings protocol SET_SCREEN_LIST command (message type 1292).
|
||||
*
|
||||
* <p>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).
|
||||
*
|
||||
* <p><b>ScanWatch screen IDs</b> 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.
|
||||
*
|
||||
* <p><b>Steel HR screen IDs</b> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -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() {}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
+7
@@ -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<WithingsStructure> structures = dataStructureFactory.createStructuresFromRawData(rawStructureData);
|
||||
|
||||
+5
@@ -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() {}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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<Short, IncomingMessageHandler> 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);
|
||||
}
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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<String, byte[]> appIconCache = new HashMap<>();
|
||||
|
||||
public NotificationRequestHandler(WithingsSteelHRDeviceSupport support) {
|
||||
public NotificationRequestHandler(WithingsBaseDeviceSupport support) {
|
||||
this.support = support;
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -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<Integer, NotificationSpec> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1834,6 +1834,86 @@
|
||||
<item>other</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Withings ScanWatch screen list (drag-to-reorder) -->
|
||||
<string-array name="pref_withings_scanwatch_screens">
|
||||
<item>@string/withings_scanwatch_screen_date</item>
|
||||
<item>@string/withings_scanwatch_screen_sleep</item>
|
||||
<item>@string/withings_scanwatch_screen_ecg</item>
|
||||
<item>@string/withings_scanwatch_screen_elevation</item>
|
||||
<item>@string/withings_scanwatch_screen_heart_rate</item>
|
||||
<item>@string/withings_scanwatch_screen_spo2</item>
|
||||
<item>@string/withings_scanwatch_screen_calories</item>
|
||||
<item>@string/withings_scanwatch_screen_settings</item>
|
||||
<item>@string/withings_scanwatch_screen_workouts</item>
|
||||
<item>@string/withings_scanwatch_screen_distance</item>
|
||||
<item>@string/withings_scanwatch_screen_steps</item>
|
||||
<item>@string/withings_scanwatch_screen_breathe</item>
|
||||
<item>@string/withings_scanwatch_screen_clock</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_withings_scanwatch_screens_values">
|
||||
<item>date</item>
|
||||
<item>sleep</item>
|
||||
<item>ecg</item>
|
||||
<item>elevation</item>
|
||||
<item>heart_rate</item>
|
||||
<item>spo2</item>
|
||||
<item>calories</item>
|
||||
<item>settings</item>
|
||||
<item>workouts</item>
|
||||
<item>distance</item>
|
||||
<item>steps</item>
|
||||
<item>breathe</item>
|
||||
<item>clock</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Default order: all 13 screens enabled in the confirmed app order -->
|
||||
<string-array name="pref_withings_scanwatch_screens_default">
|
||||
<item>date</item>
|
||||
<item>sleep</item>
|
||||
<item>ecg</item>
|
||||
<item>elevation</item>
|
||||
<item>heart_rate</item>
|
||||
<item>spo2</item>
|
||||
<item>calories</item>
|
||||
<item>settings</item>
|
||||
<item>workouts</item>
|
||||
<item>distance</item>
|
||||
<item>steps</item>
|
||||
<item>breathe</item>
|
||||
<item>clock</item>
|
||||
</string-array>
|
||||
|
||||
<!-- Long-press crown shortcut options.
|
||||
ECG (1) and SpO2 (2) require the user to have accepted the health terms & conditions
|
||||
in the Withings app before they can be activated via BLE - see ShortcutAction. -->
|
||||
<string-array name="pref_withings_scanwatch_shortcut_entries">
|
||||
<item>@string/withings_scanwatch_shortcut_none</item>
|
||||
<item>@string/withings_scanwatch_shortcut_ecg</item>
|
||||
<item>@string/withings_scanwatch_shortcut_spo2</item>
|
||||
<item>@string/withings_scanwatch_shortcut_workout_start</item>
|
||||
<item>@string/withings_scanwatch_shortcut_workout_selection</item>
|
||||
<item>@string/withings_scanwatch_shortcut_breathe</item>
|
||||
<item>@string/withings_scanwatch_shortcut_stopwatch</item>
|
||||
<item>@string/withings_scanwatch_shortcut_timer</item>
|
||||
<item>@string/withings_scanwatch_shortcut_dnd</item>
|
||||
<item>@string/withings_scanwatch_shortcut_quicklook</item>
|
||||
</string-array>
|
||||
<!-- Values are the string representations of the action bytes (0-9).
|
||||
10 = FINDMYPHONE and 11 = FLASHLIGHT are intentionally omitted (unsupported). -->
|
||||
<string-array name="pref_withings_scanwatch_shortcut_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="pref_neo_display_items">
|
||||
<item>@string/menuitem_pai</item>
|
||||
<item>@string/menuitem_dnd</item>
|
||||
|
||||
@@ -3486,6 +3486,7 @@
|
||||
<string name="devicetype_onemore_sonoflow" translatable="false">1MORE SonoFlow</string>
|
||||
<string name="devicetype_sony_wena3" translatable="false">Sony Wena 3</string>
|
||||
<string name="devicetype_withings_steel_hr" translatable="false">Withings Steel HR</string>
|
||||
<string name="devicetype_withings_scanwatch" translatable="false">Withings Scanwatch</string>
|
||||
<string name="pref_button_action_disabled">Disabled</string>
|
||||
<string name="pref_media_play">Media Play</string>
|
||||
<string name="pref_media_pause">Media Pause</string>
|
||||
@@ -3736,6 +3737,37 @@
|
||||
<string name="withings_calibration_text_activity_target">Finally align the activity hand to 100%. Please be aware that this hand only moves clockwise.</string>
|
||||
<string name="withings_bt_calibration_previous">Previous</string>
|
||||
<string name="withings_bt_calibration_next">Next</string>
|
||||
<string name="withings_scanwatch_pref_screens_title">Watch screens</string>
|
||||
<string name="withings_scanwatch_pref_screens_summary">Choose which screens appear on the watch and drag to reorder them</string>
|
||||
<string name="withings_scanwatch_screen_date">Date</string>
|
||||
<string name="withings_scanwatch_screen_sleep">Sleep</string>
|
||||
<string name="withings_scanwatch_screen_ecg">ECG</string>
|
||||
<string name="withings_scanwatch_screen_elevation">Elevation</string>
|
||||
<string name="withings_scanwatch_screen_heart_rate">Heart rate</string>
|
||||
<string name="withings_scanwatch_screen_spo2">SpO2 / Blood oxygen</string>
|
||||
<string name="withings_scanwatch_screen_calories">Calories</string>
|
||||
<string name="withings_scanwatch_screen_settings">Settings</string>
|
||||
<string name="withings_scanwatch_screen_workouts">Workouts</string>
|
||||
<string name="withings_scanwatch_screen_distance">Distance</string>
|
||||
<string name="withings_scanwatch_screen_steps">Steps</string>
|
||||
<string name="withings_scanwatch_screen_breathe">Breathe</string>
|
||||
<string name="withings_scanwatch_screen_clock">Clock</string>
|
||||
|
||||
<!-- Shortcut (long-press crown) settings -->
|
||||
<string name="withings_scanwatch_pref_shortcut_title">Long-press crown shortcut</string>
|
||||
<string name="withings_scanwatch_pref_shortcut_summary">Action launched by holding the crown button</string>
|
||||
<string name="withings_scanwatch_shortcut_none">None</string>
|
||||
<string name="withings_scanwatch_shortcut_ecg">ECG measurement</string>
|
||||
<string name="withings_scanwatch_shortcut_spo2">SpO2 / Blood oxygen measurement</string>
|
||||
<string name="withings_scanwatch_shortcut_workout_start">Start workout</string>
|
||||
<string name="withings_scanwatch_shortcut_workout_selection">Workout selection</string>
|
||||
<string name="withings_scanwatch_shortcut_breathe">Breathe</string>
|
||||
<string name="withings_scanwatch_shortcut_stopwatch">Stopwatch</string>
|
||||
<string name="withings_scanwatch_shortcut_timer">Timer</string>
|
||||
<string name="withings_scanwatch_shortcut_dnd">Do not disturb</string>
|
||||
<string name="withings_scanwatch_shortcut_quicklook">Quick look</string>
|
||||
<string name="withings_scanwatch_shortcut_findmyphone">Find my phone</string>
|
||||
<string name="withings_scanwatch_shortcut_flashlight">Flashlight</string>
|
||||
<string name="drag_handle">drag handle</string>
|
||||
<string name="find_my_phone_found_it">FOUND IT</string>
|
||||
<string name="pref_activity_full_sync_trigger_summary">Trigger a full sync of all activity data</string>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<com.mobeta.android.dslv.DragSortListPreference
|
||||
android:icon="@drawable/ic_activity_unknown_small"
|
||||
android:defaultValue="@array/pref_withings_steel_activity_types_default"
|
||||
android:dialogTitle="@string/mi5_prefs_workout_activity_types"
|
||||
android:entries="@array/pref_withings_steel_activity_types"
|
||||
android:entryValues="@array/pref_withings_steel_activity_types_values"
|
||||
android:key="workout_activity_types_sortable"
|
||||
android:persistent="true"
|
||||
android:summary="@string/mi5_prefs_workout_activity_types_summary"
|
||||
android:title="@string/mi5_prefs_workout_activity_types" />
|
||||
<Preference
|
||||
android:title="@string/qhybrid_title_calibration"
|
||||
android:icon="@drawable/ic_sensor_calibration"
|
||||
android:summary="@string/qhybrid_summary_calibration"
|
||||
android:key="hands_calibration">
|
||||
<intent
|
||||
android:targetPackage="@string/applicationId"
|
||||
android:targetClass="nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsCalibrationActivity" />
|
||||
</Preference>
|
||||
|
||||
<!-- Watch screen configuration: drag to reorder, uncheck to disable -->
|
||||
<com.mobeta.android.dslv.DragSortListPreference
|
||||
android:icon="@drawable/ic_watchface"
|
||||
android:defaultValue="@array/pref_withings_scanwatch_screens_default"
|
||||
android:dialogTitle="@string/withings_scanwatch_pref_screens_title"
|
||||
android:entries="@array/pref_withings_scanwatch_screens"
|
||||
android:entryValues="@array/pref_withings_scanwatch_screens_values"
|
||||
android:key="withings_scanwatch_screens_sortable"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_screens_summary"
|
||||
android:title="@string/withings_scanwatch_pref_screens_title" />
|
||||
|
||||
<!-- Long-press crown shortcut -->
|
||||
<ListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/pref_withings_scanwatch_shortcut_entries"
|
||||
android:entryValues="@array/pref_withings_scanwatch_shortcut_values"
|
||||
android:key="withings_scanwatch_shortcut_action"
|
||||
android:persistent="true"
|
||||
android:summary="@string/withings_scanwatch_pref_shortcut_summary"
|
||||
android:title="@string/withings_scanwatch_pref_shortcut_title" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
+1
@@ -19,6 +19,7 @@ public class AbstractDeviceCoordinatorTest extends TestBase {
|
||||
final Map<String, DeviceType> 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
|
||||
|
||||
Reference in New Issue
Block a user