Support for Withings Steel HR (#2831)

Co-authored-by: hrglpfrmpf <hrglpfrmpf@web.de>
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2831
Co-authored-by: hrglpfrmpf <hrglpfrmpf@noreply.codeberg.org>
Co-committed-by: hrglpfrmpf <hrglpfrmpf@noreply.codeberg.org>
This commit is contained in:
hrglpfrmpf 2023-07-26 17:20:43 +00:00 committed by José Rebelo
parent 606e20a065
commit c1fd0b77ad
119 changed files with 8337 additions and 6 deletions

View File

@ -90,6 +90,7 @@ public class GBDaoGenerator {
addCasioGBX100Sample(schema, user, device);
addFitProActivitySample(schema, user, device);
addPineTimeActivitySample(schema, user, device);
addWithingsSteelHRActivitySample(schema, user, device);
addHybridHRActivitySample(schema, user, device);
addVivomoveHrActivitySample(schema, user, device);
addGarminFitFile(schema, user, device);
@ -818,7 +819,7 @@ public class GBDaoGenerator {
Property deviceId = batteryLevel.addLongProperty("deviceId").primaryKey().notNull().getProperty();
batteryLevel.addToOne(device, deviceId);
batteryLevel.addIntProperty("level").notNull();
batteryLevel.addIntProperty("batteryIndex").notNull().primaryKey();;
batteryLevel.addIntProperty("batteryIndex").notNull().primaryKey();
return batteryLevel;
}
@ -847,4 +848,18 @@ public class GBDaoGenerator {
addHeartRateProperties(activitySample);
return activitySample;
}
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();
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
activitySample.addIntProperty("distance").notNull();
activitySample.addIntProperty("calories").notNull();
addHeartRateProperties(activitySample);
activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE);
return activitySample;
}
}

View File

@ -722,6 +722,10 @@
android:name=".devices.qhybrid.CalibrationActivity"
android:label="@string/qhybrid_title_calibration"
android:parentActivityName=".devices.qhybrid.HRConfigActivity" />
<activity
android:name=".devices.withingssteelhr.WithingsCalibrationActivity"
android:label="@string/qhybrid_title_calibration"
android:parentActivityName=".devices.withingssteelhr.WithingsCalibrationActivity" />
<activity
android:name=".devices.qhybrid.CommuteActionsActivity"
android:label="@string/qhybrid_pref_title_actions"

View File

@ -0,0 +1,201 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import nodomain.freeyourgadget.gadgetbridge.R;
public class RotaryControl extends View {
public interface RotationListener {
void onRotation(short movementAmount);
}
private Path circlePath;
private int controlPointX;
private int controlPointY;
private int controlCenterX;
private int controlCenterY;
private int controlRadius;
private int padding;
private int controlPointSize;
private int controlPointColor;
private int lineColor;
private int lineThickness;
private double startAngle;
private double angle ;
private boolean isControlPointSelected = false;
private Paint paint = new Paint();
private Paint controlPointPaint = new Paint();
private RotationListener rotationListener;
public RotaryControl(Context context) {
this(context, null);
}
public RotaryControl(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RotaryControl(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs, defStyleAttr);
}
private void init(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RotaryControl, defStyleAttr, 0);
startAngle = a.getFloat(R.styleable.RotaryControl_start_angle, (float) Math.PI / 2);
angle = startAngle;
controlPointSize = a.getDimensionPixelSize(R.styleable.RotaryControl_controlpoint_size, 50);
controlPointColor = a.getColor(R.styleable.RotaryControl_controlpoint_color, Color.GRAY);
lineThickness = a.getDimensionPixelSize(R.styleable.RotaryControl_line_thickness, 20);
lineColor = a.getColor(R.styleable.RotaryControl_line_color, Color.RED);
calculateAndSetPadding();
a.recycle();
}
private void calculateAndSetPadding() {
int totalPadding = getPaddingLeft() + getPaddingRight() + getPaddingBottom() + getPaddingTop() + getPaddingEnd() + getPaddingStart();
padding = totalPadding / 6;
}
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
int smallerDim = width > height ? height : width;
int largestCenteredSquareLeft = (width - smallerDim) / 2;
int largestCenteredSquareTop = (height - smallerDim) / 2;
int largestCenteredSquareRight = largestCenteredSquareLeft + smallerDim;
int largestCenteredSquareBottom = largestCenteredSquareTop + smallerDim;
controlCenterX = largestCenteredSquareRight / 2 + (width - largestCenteredSquareRight) / 2;
controlCenterY = largestCenteredSquareBottom / 2 + (height - largestCenteredSquareBottom) / 2;
controlRadius = smallerDim / 2 - lineThickness / 2 - padding;
super.onSizeChanged(width, height, oldWidth, oldHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawRotationCircle(canvas);
drawControlPoint(canvas);
}
private void drawControlPoint(Canvas canvas) {
controlPointX = (int) (controlCenterX + controlRadius * Math.cos(angle));
controlPointY = (int) (controlCenterY - controlRadius * Math.sin(angle));
controlPointPaint.setColor(controlPointColor);
controlPointPaint.setStyle(Paint.Style.FILL);
controlPointPaint.setAlpha(128);
Path controlPointPath = new Path();
controlPointPath.addCircle(controlPointX, controlPointY, controlPointSize, Path.Direction.CW);
canvas.drawPath(controlPointPath, controlPointPaint);
}
private void drawRotationCircle(Canvas canvas) {
DashPathEffect dashPath = new DashPathEffect(new float[]{8,22}, (float)1.0);
paint.setPathEffect(dashPath);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(lineThickness);
paint.setAntiAlias(true);
paint.setColor(lineColor);
circlePath = new Path();
circlePath.addCircle(controlCenterX, controlCenterY, controlRadius, Path.Direction.CW);
canvas.drawPath(circlePath, paint);
}
private void updateRotationPosition(double touchX, double touchY) {
double distanceX = touchX - controlCenterX;
double distanceY = controlCenterY - touchY;
double c = Math.sqrt(Math.pow(distanceX, 2) + Math.pow(distanceY, 2));
double currentAngle = Math.acos(distanceX / c);
if (distanceY < 0) {
currentAngle = -currentAngle;
}
int movementAmount = (int) ((currentAngle - angle) * 100);
int i = (int) movementAmount;
if (movementAmount != 0) {
if (Math.abs(movementAmount) > 15) {
movementAmount /= movementAmount;
}
rotationListener.onRotation((short) -movementAmount);
}
angle = currentAngle;
}
public void setRotationListener(RotationListener listener) {
rotationListener = listener;
}
public void reset() {
angle = startAngle;
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
double x = ev.getX();
double y = ev.getY();
if (x < controlPointX + controlPointSize && x > controlPointX - controlPointSize && y < controlPointY + controlPointSize && y > controlPointY - controlPointSize) {
getParent().requestDisallowInterceptTouchEvent(true);
isControlPointSelected = true;
updateRotationPosition(x, y);
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (isControlPointSelected) {
double x = ev.getX();
double y = ev.getY();
updateRotationPosition(x, y);
}
break;
}
case MotionEvent.ACTION_UP: {
getParent().requestDisallowInterceptTouchEvent(false);
isControlPointSelected = false;
break;
}
}
invalidate();
return true;
}
}

View File

@ -0,0 +1,159 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.content.Intent;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
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;
public class WithingsCalibrationActivity extends AbstractGBActivity {
enum Hands {
HOURS((short)1),
MINUTES((short)0),
ACTIVITY_TARGET((short)2);
private short code;
private Hands(short code) {
this.code = code;
}
}
private GBDevice device;
private LocalBroadcastManager localBroadcastManager;
private String[] calibrationAdvices = new String[3];
private Hands[] hands = new Hands[]{Hands.HOURS, Hands.MINUTES, Hands.ACTIVITY_TARGET};
private short handIndex = 0;
private Button previousButton;
private Button nextButton;
private Button okButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_withings_calibration);
List<GBDevice> devices = GBApplication.app().getDeviceManager().getSelectedDevices();
for(GBDevice device : devices){
if(device.getType() == DeviceType.WITHINGS_STEEL_HR ){
this.device = device;
break;
}
}
if (device == null) {
Toast.makeText(this, R.string.watch_not_connected, Toast.LENGTH_LONG).show();
finish();
return;
}
initView();
localBroadcastManager = LocalBroadcastManager.getInstance(this);
localBroadcastManager.sendBroadcast(new Intent(WithingsSteelHRDeviceSupport.START_HANDS_CALIBRATION_CMD));
}
@Override
protected void onDestroy() {
super.onDestroy();
if (localBroadcastManager != null) {
localBroadcastManager.sendBroadcast(new Intent(WithingsSteelHRDeviceSupport.STOP_HANDS_CALIBRATION_CMD));
}
}
private void initView() {
calibrationAdvices[0] = getString(R.string.withings_calibration_text_hours);
calibrationAdvices[1] = getString(R.string.withings_calibration_text_minutes);
calibrationAdvices[2] = getString(R.string.withings_calibration_text_activity_target);
RotaryControl rotaryControl = findViewById(R.id.rotary_control);
rotaryControl.setRotationListener(new RotaryControl.RotationListener() {
@Override
public void onRotation(short movementAmount) {
Intent calibration = new Intent(WithingsSteelHRDeviceSupport.HANDS_CALIBRATION_CMD);
calibration.putExtra("hand", hands[handIndex].code);
calibration.putExtra("movementAmount", movementAmount);
localBroadcastManager.sendBroadcast(calibration);
}
});
TextView textView = findViewById(R.id.withings_calibration_textview);
textView.setText(calibrationAdvices[0]);
previousButton = findViewById(R.id.withings_calibration_button_previous);
previousButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handIndex--;
enableButtons();
textView.setText(calibrationAdvices[handIndex]);
rotaryControl.reset();
}
});
nextButton = findViewById(R.id.withings_calibration_button_next);
nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handIndex++;
enableButtons();
textView.setText(calibrationAdvices[handIndex]);
rotaryControl.reset();
}
});
okButton = findViewById(R.id.withings_calibration_button_ok);
okButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
enableButtons();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return super.onOptionsItemSelected(item);
}
private void enableButtons() {
nextButton.setEnabled(handIndex < 2);
previousButton.setEnabled(handIndex > 0);
okButton.setEnabled(handIndex == 2);
}
}

View File

@ -0,0 +1,175 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Locale;
import de.greenrobot.dao.query.QueryBuilder;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class WithingsSteelHRDeviceCoordinator extends AbstractDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
Long deviceId = device.getId();
QueryBuilder<?> qb = session.getWithingsSteelHRActivitySampleDao().queryBuilder();
qb.where(WithingsSteelHRActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
}
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
String name = candidate.getDevice().getName();
if (name != null && (name.toLowerCase(Locale.ROOT).startsWith("steel") || name.toLowerCase(Locale.ROOT).startsWith("activite"))) {
return DeviceType.WITHINGS_STEEL_HR;
}
return DeviceType.UNKNOWN;
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[]{
R.xml.devicesettings_withingssteelhr
};
}
@Override
public DeviceType getDeviceType() {
return DeviceType.WITHINGS_STEEL_HR;
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_BOND;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public boolean supportsRemSleep() {
return true;
}
@Override
public boolean supportsActivityDataFetching() {
return true;
}
@Override
public boolean supportsActivityTracking() {
return true;
}
@Override
public boolean supportsActivityTracks() {
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return new WithingsSteelHRSampleProvider(device, session);
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public int getAlarmSlotCount(GBDevice gbDevice) {
return 3;
}
@Override
public boolean supportsAlarmDescription(GBDevice device) {
return true;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return true;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return true;
}
@Override
public String getManufacturer() {
return "Withings";
}
@Override
public boolean supportsAppsManagement(GBDevice gbDevice) {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return true;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
return false;
}
}

View File

@ -0,0 +1,110 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
import de.greenrobot.dao.AbstractDao;
import de.greenrobot.dao.Property;
import de.greenrobot.dao.query.Query;
import de.greenrobot.dao.query.QueryBuilder;
import de.greenrobot.dao.query.WhereCondition;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySampleDao;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.ActivitySampleHandler;
public class WithingsSteelHRSampleProvider extends AbstractSampleProvider<WithingsSteelHRActivitySample> {
private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRSampleProvider.class);
public WithingsSteelHRSampleProvider(GBDevice device, DaoSession session) {
super(device, session);
}
@Override
public AbstractDao<WithingsSteelHRActivitySample, ?> getSampleDao() {
return getSession().getWithingsSteelHRActivitySampleDao();
}
@Nullable
@Override
protected Property getRawKindSampleProperty() {
return WithingsSteelHRActivitySampleDao.Properties.RawKind;
}
@NonNull
@Override
protected Property getTimestampSampleProperty() {
return WithingsSteelHRActivitySampleDao.Properties.Timestamp;
}
@NonNull
@Override
protected Property getDeviceIdentifierSampleProperty() {
return WithingsSteelHRActivitySampleDao.Properties.DeviceId;
}
@Override
public List<WithingsSteelHRActivitySample> getActivitySamples(int timestamp_from, int timestamp_to) {
return super.getGBActivitySamples(timestamp_from, timestamp_to, ActivityKind.TYPE_ALL);
}
@Override
public int normalizeType(int rawType) {
return rawType;
}
@Override
public int toRawActivityKind(int activityKind) {
switch (activityKind) {
case ActivityKind.TYPE_UNKNOWN:
return 0;
case ActivityKind.TYPE_LIGHT_SLEEP:
return 1;
case ActivityKind.TYPE_DEEP_SLEEP:
return 2;
default:
return activityKind;
}
}
@Override
public float normalizeIntensity(int rawIntensity) {
if (rawIntensity > 0) {
return (float) (Math.log(rawIntensity) / 8);
}
return 0;
}
@Override
public WithingsSteelHRActivitySample createActivitySample() {
return new WithingsSteelHRActivitySample();
}
}

View File

@ -138,6 +138,7 @@ public enum DeviceType {
SUPER_CARS(530, R.drawable.ic_device_supercars, R.drawable.ic_device_supercars_disabled, R.string.devicetype_super_cars),
ASTEROIDOS(540, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_asteroidos),
SOFLOW_SO6(550, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_soflow_s06),
WITHINGS_STEEL_HR(560, R.drawable.ic_device_watchxplus, R.drawable.ic_device_watchxplus_disabled, R.string.withings_steel_hr),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
private final int key;

View File

@ -111,6 +111,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.Vibrati
import nodomain.freeyourgadget.gadgetbridge.service.devices.vivomovehr.VivomoveHrSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.waspos.WaspOSDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
@ -375,6 +376,8 @@ public class DeviceSupportFactory {
return new ServiceDeviceSupport(new AsteroidOSDeviceSupport());
case SOFLOW_SO6:
return new ServiceDeviceSupport(new SoFlowSupport());
case WITHINGS_STEEL_HR:
return new ServiceDeviceSupport(new WithingsSteelHRDeviceSupport());
case VIVOMOVE_HR:
return new ServiceDeviceSupport(new VivomoveHrSupport());
}

View File

@ -26,6 +26,8 @@ import org.slf4j.LoggerFactory;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.NotifyAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.ReadAction;
import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.RequestConnectionPriorityAction;
@ -61,6 +63,18 @@ public class TransactionBuilder {
return add(action);
}
public TransactionBuilder writeChunkedData(BluetoothGattCharacteristic characteristic, byte[] data, int chunkSize) {
for (int start = 0; start < data.length; start += chunkSize) {
int end = start + chunkSize;
if (end > data.length) end = data.length;
WriteAction action = new WriteAction(characteristic, Arrays.copyOfRange(data, start, end));
add(action);
}
return this;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public TransactionBuilder requestMtu(int mtu){
return add(
new RequestMtuAction(mtu)

View File

@ -0,0 +1,128 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
import android.bluetooth.BluetoothGattCharacteristic;
import android.os.Build;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Random;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation.AbstractResponseHandler;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.Challenge;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ChallengeResponse;
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.ProbeReply;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class AuthenticationHandler extends AbstractResponseHandler {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationHandler.class);
// TODO: Save this somewhere if we actually decide to use te secret for more security:
private final String secret = "2EM5zNP37QzM00hmP6BFTD92nG15XwNd";
private WithingsSteelHRDeviceSupport support;
private Challenge challengeToSend;
public AuthenticationHandler(WithingsSteelHRDeviceSupport support) {
super(support);
this.support = support;
}
@Override
public void handleResponse(Message response) {
short messageType = response.getType();
if (messageType == WithingsMessageType.PROBE) {
handleProbeReply(response);
} else if (messageType == WithingsMessageType.CHALLENGE) {
handleChallenge(response);
} else {
logger.warn("Received unkown message: " + messageType + ", will ignore this.");
}
}
private void handleChallenge(Message challengeMessage) {
try {
Challenge challenge = getTypeFromReply(Challenge.class, challengeMessage);
ChallengeResponse challengeResponse = new ChallengeResponse();
challengeResponse.setResponse(createResponse(challenge));
Message message = new WithingsMessage(WithingsMessageType.CHALLENGE);
message.addDataStructure(challengeResponse);
challengeToSend = new Challenge();
challengeToSend.setMacAddress(challenge.getMacAddress());
byte[] bArr = new byte[16];
new Random().nextBytes(bArr);
challengeToSend.setChallenge(bArr);
message.addDataStructure(challengeToSend);
support.sendToDevice(message);
} catch (Exception e) {
logger.error("Failed to create response to challenge: " + e.getMessage());
}
}
private void handleProbeReply(Message message) {
ProbeReply probeReply = getTypeFromReply(ProbeReply.class, message);
if (probeReply == null) {
throw new IllegalArgumentException("Message does not contain the required datastructure ProbeReply");
}
ChallengeResponse response = getTypeFromReply(ChallengeResponse.class, message);
if (response == null || Arrays.equals(response.getResponse(), createResponse(challengeToSend))) {
support.getDevice().setFirmwareVersion(String.valueOf(probeReply.getFirmwareVersion()));
} else {
throw new SecurityException("Response is not the one expected!");
}
support.onAuthenticationFinished();
}
private byte[] createResponse(Challenge challenge) {
try {
ByteBuffer allocate = ByteBuffer.allocate(challenge.getChallenge().length + challenge.getMacAddress().getBytes().length + secret.getBytes().length);
allocate.put(challenge.getChallenge());
allocate.put(challenge.getMacAddress().getBytes());
allocate.put(secret.getBytes());
return MessageDigest.getInstance("SHA1").digest(allocate.array());
} catch (NoSuchAlgorithmException e) {
logger.error("Failed to create response to challenge: " + e.getMessage());
}
return new byte[0];
}
private <T extends WithingsStructure> T getTypeFromReply(Class<T> type, Message message) {
for (WithingsStructure structure : message.getDataStructures()) {
if (type.isInstance(structure)) {
return (T)structure;
}
}
return null;
}
}

View File

@ -0,0 +1,71 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import nodomain.freeyourgadget.gadgetbridge.util.BitmapUtil;
public class IconHelper {
public static byte[] getIconBytesFromDrawable(Drawable drawable) {
Bitmap bitmap = BitmapUtil.toBitmap(drawable);
Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, 22, 24, true);
int size = scaledBitmap.getRowBytes() * scaledBitmap.getHeight();
return toByteArray(scaledBitmap);
}
public static byte[] toByteArray(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int bytesPerColumn = getBytesPerColumn(height);
byte[] rawData = new byte[bytesPerColumn * width];
for (int col = 0; col < width; col++) {
for (int row = 0; row < height; row++) {
int pixel = bitmap.getPixel(col, row);
if (shouldPixelbeAdded(pixel)) {
int bitIndex = bytesPerColumn * col + row / 8;
rawData[bitIndex] = setBit(rawData[bitIndex], row);
}
}
}
return rawData;
}
private static boolean shouldPixelbeAdded(int pixel) {
double luma = ((Color.red(pixel) * 0.2126d) + (Color.green(pixel) * 0.7152d) + (Color.blue(pixel) * 0.0722d)) * (Color.alpha(pixel) / 255.0f);
return luma > 0;
}
private static byte setBit(byte bits, int position) {
bits |= 1 << (position % 8);
return bits;
}
private static int getBytesPerColumn(int rowCount) {
int result = (int) rowCount / 8;
if (result * 8 < rowCount) {
result++;
}
return result;
}
}

View File

@ -0,0 +1,725 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr;
import static nodomain.freeyourgadget.gadgetbridge.GBApplication.getContext;
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.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.hplus.HPlusConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
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.btle.actions.SetDeviceStateAction;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity.WithingsActivityType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsServerAction;
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.Prefs;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class WithingsSteelHRDeviceSupport extends AbstractBTLEDeviceSupport {
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 static Prefs prefs = GBApplication.getPrefs();
private MessageBuilder messageBuilder;
private LiveWorkoutHandler liveWorkoutHandler;
private ActivitySampleHandler activitySampleHandler;
private ConversationQueue conversationQueue;
private boolean firstTimeConnect;
private BluetoothGattCharacteristic notificationSourceCharacteristic;
private BluetoothGattCharacteristic dataSourceCharacteristic;
private BluetoothDevice device;
private boolean syncInProgress;
private ActivityUser activityUser;
private NotificationProvider notificationProvider;
private IncomingMessageHandlerFactory incomingMessageHandlerFactory;
private final BroadcastReceiver commandReceiver;
private int mtuSize = 115;
public WithingsSteelHRDeviceSupport() {
super(logger);
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);
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 boolean getSendWriteRequestResponse() {
return true;
}
@Override
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
logger.debug("Starting initialization...");
conversationQueue = new ConversationQueue(this);
builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext()));
getDevice().setFirmwareVersion("N/A");
getDevice().setFirmwareVersion2("N/A");
BluetoothGattCharacteristic characteristic = getCharacteristic(WithingsUUID.WITHINGS_WRITE_CHARACTERISTIC_UUID);
builder.notify(characteristic, true);
logger.debug("Requesting change of MTU...");
builder.requestMtu(119);
return builder;
}
@Override
public boolean connectFirstTime() {
firstTimeConnect = true;
return connect();
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
logger.debug("MTU has changed to " + mtu);
mtuSize = mtu;
if (firstTimeConnect) {
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.INITIAL_CONNECT));
addSimpleConversationToQueue(new WithingsMessage(WithingsMessageType.SET_LOCALE, new Locale("de")));
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 || !shoudSync()) {
return;
}
getDevice().setBusyTask("Syncing");
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()));
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) {
if (super.onCharacteristicChanged(gatt, characteristic)) {
return true;
}
byte[] data = characteristic.getValue();
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;
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, mtuSize - 4);
builder.queue(getQueue());
} catch (Exception e) {
logger.warn("Could not send message because of " + e.getMessage());
}
}
public void sendAncsNotificationSourceNotification(NotificationSource notificationSource) {
try {
ServerTransactionBuilder builder = performServer("notificationSourceNotification");
notificationSourceCharacteristic.setValue(notificationSource.serialize());
builder.add(new WithingsServerAction(device, notificationSourceCharacteristic));
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();
dataSourceCharacteristic.setValue(response.serialize());
builder.add(new WithingsServerAction(device, dataSourceCharacteristic));
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.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext()));
builder.queue(getQueue());
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) {
e.printStackTrace();
}
}
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 = ActivityKind.getIconId(withingsActivityType.toActivityKind());
Drawable drawable = getContext().getDrawable(drawableId);
imageData.setImageData(IconHelper.getIconBytesFromDrawable(drawable));
message.addDataStructure(imageData);
return message;
}
private short getTimeMode() {
GBPrefs gbPrefs = new GBPrefs(new Prefs(GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress())));
String tmode = gbPrefs.getTimeFormat();
if ("24h".equals(tmode)) {
return UserUnitConstants.UNIT_24H;
} else {
return UserUnitConstants.UNIT_12H;
}
}
private short getUnit() {
String units = prefs.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;
}
}
}

View File

@ -0,0 +1,101 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity;
public class ActivityEntry {
private int timestamp;
private int duration;
private int rawKind = -1;
private int heartrate;
private int steps;
private int calories;
private int distance;
private int rawIntensity;
private boolean isHeartrate;
public int getTimestamp() {
return timestamp;
}
public void setTimestamp(int timestamp) {
this.timestamp = timestamp;
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
public int getHeartrate() {
return heartrate;
}
public void setIsHeartrate(int heartrate) {
this.heartrate = heartrate;
}
public int getRawKind() {
return rawKind;
}
public void setRawKind(int rawKind) {
this.rawKind = rawKind;
}
public int getSteps() {
return steps;
}
public void setSteps(int steps) {
this.steps = steps;
}
public int getCalories() {
return calories;
}
public void setCalories(int calories) {
this.calories = calories;
}
public int getDistance() {
return distance;
}
public void setDistance(int distance) {
this.distance = distance;
}
public int getRawIntensity() {
return rawIntensity;
}
public void setRawIntensity(int rawIntensity) {
this.rawIntensity = rawIntensity;
}
public boolean isHeartrate() {
return isHeartrate;
}
public void setIsHeartrate(boolean heartrate) {
isHeartrate = heartrate;
}
}

View File

@ -0,0 +1,93 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
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.
*/
public class SleepActivitySampleHelper {
private static Logger logger = LoggerFactory.getLogger(SleepActivitySampleHelper.class);
private static int mergeCount;
public static WithingsSteelHRActivitySample mergeIfNecessary(WithingsSteelHRSampleProvider provider, WithingsSteelHRActivitySample sample) {
if (!shouldMerge(sample)) {
return sample;
}
WithingsSteelHRActivitySample overlappingSample = getOverlappingSample(provider, (int)sample.getTimestamp());
if (overlappingSample != null) {
sample = doMerge(overlappingSample, sample);
}
return sample;
}
private static WithingsSteelHRActivitySample getOverlappingSample(WithingsSteelHRSampleProvider provider, long timestamp) {
List<WithingsSteelHRActivitySample> 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);
if (isNotHeartRateOnly(lastSample, (int) timestamp)) {
return lastSample;
}
}
return null;
}
private static boolean isNotHeartRateOnly(WithingsSteelHRActivitySample lastSample, int timestamp) {
return lastSample.getRawKind() != ActivityKind.TYPE_NOT_MEASURED; // && lastSample.getTimestamp() <= timestamp && (lastSample.getTimestamp() + lastSample.getDuration()) >= timestamp);
}
private static boolean shouldMerge(WithingsSteelHRActivitySample sample) {
return sample.getSteps() == 0
&& sample.getDistance() == 0
&& sample.getRawKind() == -1
&& sample.getCalories() == 0
&& sample.getHeartRate() > 1
&& sample.getRawIntensity() == 0;
}
private static WithingsSteelHRActivitySample doMerge(WithingsSteelHRActivitySample origin, WithingsSteelHRActivitySample update) {
WithingsSteelHRActivitySample mergeResult = new WithingsSteelHRActivitySample();
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());
return mergeResult;
}
}

View File

@ -0,0 +1,168 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.activity;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiWorkoutScreenActivityType;
public enum WithingsActivityType {
WALKING(1),
RUNNING(2),
HIKING(3),
BIKING(6),
SWIMMING(7),
SURFING(8),
KITESURFING(9),
WINDSURFING(10),
TENNIS(12),
PINGPONG(13),
SQUASH(14),
BADMINTON(15),
WEIGHTLIFTING(16),
GYMNASTICS(17),
ELLIPTICAL(18),
PILATES(19),
BASKETBALL(20),
SOCCER(21),
FOOTBALL(22),
RUGBY(23),
VOLLEYBALL(24),
GOLFING(227),
YOGA(28),
DANCING(29),
BOXING(30),
SKIING(34),
SNOWBOARDING(35),
ROWING(0), // The code has yet to be identified.
ZUMBA(188),
BASEBALL(191),
HANDBALL(192),
HOCKEY(193),
ICEHOCKEY(194),
CLIMBING(195),
ICESKATING(196),
RIDING(26),
OTHER(36);
private int code;
WithingsActivityType(int typeCode) {
this.code = typeCode;
}
public static WithingsActivityType fromCode(int withingsCode) {
for (WithingsActivityType type : values()) {
if (type.code == withingsCode) {
return type;
}
}
throw new RuntimeException("No matching WithingsActivityType for code: " + withingsCode);
}
public int getCode() {
return code;
}
public int toActivityKind() {
switch (this) {
case WALKING:
return ActivityKind.TYPE_WALKING;
case RUNNING:
return ActivityKind.TYPE_RUNNING;
case HIKING:
return ActivityKind.TYPE_HIKING;
case BIKING:
return ActivityKind.TYPE_CYCLING;
case SWIMMING:
return ActivityKind.TYPE_SWIMMING;
case SURFING:
return ActivityKind.TYPE_ACTIVITY;
case KITESURFING:
return ActivityKind.TYPE_ACTIVITY;
case WINDSURFING:
return ActivityKind.TYPE_ACTIVITY;
case TENNIS:
return ActivityKind.TYPE_ACTIVITY;
case PINGPONG:
return ActivityKind.TYPE_PINGPONG;
case SQUASH:
return ActivityKind.TYPE_ACTIVITY;
case BADMINTON:
return ActivityKind.TYPE_BADMINTON;
case WEIGHTLIFTING:
return ActivityKind.TYPE_ACTIVITY;
case GYMNASTICS:
return ActivityKind.TYPE_EXERCISE;
case ELLIPTICAL:
return ActivityKind.TYPE_ELLIPTICAL_TRAINER;
case PILATES:
return ActivityKind.TYPE_YOGA;
case BASKETBALL:
return ActivityKind.TYPE_BASKETBALL;
case SOCCER:
return ActivityKind.TYPE_SOCCER;
case FOOTBALL:
return ActivityKind.TYPE_ACTIVITY;
case RUGBY:
return ActivityKind.TYPE_ACTIVITY;
case VOLLEYBALL:
return ActivityKind.TYPE_ACTIVITY;
case GOLFING:
return ActivityKind.TYPE_ACTIVITY;
case YOGA:
return ActivityKind.TYPE_YOGA;
case DANCING:
return ActivityKind.TYPE_ACTIVITY;
case BOXING:
return ActivityKind.TYPE_ACTIVITY;
case SKIING:
return ActivityKind.TYPE_ACTIVITY;
case SNOWBOARDING:
return ActivityKind.TYPE_ACTIVITY;
case ROWING:
return ActivityKind.TYPE_ROWING_MACHINE;
case ZUMBA:
return ActivityKind.TYPE_ACTIVITY;
case BASEBALL:
return ActivityKind.TYPE_CRICKET;
case HANDBALL:
return ActivityKind.TYPE_ACTIVITY;
case HOCKEY:
return ActivityKind.TYPE_ACTIVITY;
case ICEHOCKEY:
return ActivityKind.TYPE_ACTIVITY;
case CLIMBING:
return ActivityKind.TYPE_CLIMBING;
case ICESKATING:
return ActivityKind.TYPE_ACTIVITY;
default:
return ActivityKind.TYPE_UNKNOWN;
}
}
public static WithingsActivityType fromPrefValue(final String prefValue) {
for (final WithingsActivityType type : values()) {
if (type.name().toLowerCase(Locale.ROOT).equals(prefValue.replace("_", "").toLowerCase(Locale.ROOT))) {
return type;
}
}
throw new RuntimeException("No matching WithingsActivityType for pref value: " + prefValue);
}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattServer;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BtLEServerAction;
public class WithingsServerAction extends BtLEServerAction
{
private BluetoothGattCharacteristic characteristic;
public WithingsServerAction(BluetoothDevice device, BluetoothGattCharacteristic characteristic) {
super(device);
this.characteristic = characteristic;
}
@Override
public boolean expectsResult() {
return false;
}
@Override
public boolean run(BluetoothGattServer server) {
return server.notifyCharacteristicChanged(getDevice(), characteristic, false);
}
}

View File

@ -0,0 +1,34 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication;
import java.util.UUID;
public final class WithingsUUID {
public static final UUID WITHINGS_SERVICE_UUID = UUID.fromString("00000020-5749-5448-0037-000000000000");
public static final UUID WITHINGS_WRITE_CHARACTERISTIC_UUID = UUID.fromString("00000024-5749-5448-0037-000000000000");
public static final UUID WITHINGS_APP_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-0037-000000000000");
public static final UUID WITHINGS_APP_CHARACTERISTIC2_UUID = UUID.fromString("10000028-5749-5448-0037-000000000000");
public static final UUID CCC_DESCRIPTOR_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
public static final UUID WITHINGS_ANCS_SERVICE_UUID = UUID.fromString("10000057-5749-5448-0037-00000000000000");
public static final UUID NOTIFICATION_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("10000059-5749-5448-0037-00000000000000");
public static final UUID CONTROL_POINT_CHARACTERISTIC_UUID = UUID.fromString("10000058-5749-5448-0037-00000000000000");
public static final UUID DATA_SOURCE_CHARACTERISTIC_UUID = UUID.fromString("1000005a-5749-5448-0037-00000000000000");
private WithingsUUID() {}
}

View File

@ -0,0 +1,107 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructureType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public abstract class AbstractConversation implements Conversation {
private List<ConversationObserver> observers = new ArrayList();
private boolean complete;
protected Message request;
private short requestType;
protected ResponseHandler responseHandler;
public AbstractConversation(ResponseHandler responseHandler) {
this.responseHandler = responseHandler;
}
@Override
public void registerObserver(ConversationObserver observer) {
observers.add(observer);
}
@Override
public void removeObserver(ConversationObserver observer) {
observers.remove(observer);
}
@Override
public void setRequest(Message message) {
this.request = message;
this.requestType = message.getType();
}
@Override
public Message getRequest() {
return request;
}
@Override
public void handleResponse(Message response) {
if (response.getType() == requestType) {
if (request.needsResponse()) {
complete = true;
} else if (request.needsEOT()) {
complete = hasEOT(response);
}
doHandleResponse(response);
if (complete) {
notifyObservers(requestType);
}
}
}
@Override
public boolean isComplete() {
return complete;
}
protected void notifyObservers(short messageType) {
for (ConversationObserver observer : observers) {
observer.onConversationCompleted(messageType);
}
}
private boolean hasEOT(Message message) {
List<WithingsStructure> dataList = message.getDataStructures();
if (dataList != null) {
for (WithingsStructure strucuter :
dataList) {
if (strucuter.getType() == WithingsStructureType.END_OF_TRANSMISSION) {
return true;
}
}
}
return false;
}
protected abstract void doSendRequest(Message message);
protected abstract void doHandleResponse(Message message);
}

View File

@ -0,0 +1,30 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
public abstract class AbstractResponseHandler implements ResponseHandler {
protected GBDevice device;
protected WithingsSteelHRDeviceSupport support;
public AbstractResponseHandler(WithingsSteelHRDeviceSupport support) {
this.support = support;
this.device = support.getDevice();
}
}

View File

@ -0,0 +1,269 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRSampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.WithingsSteelHRActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
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;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleCalories;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleCalories2;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleDuration;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleMovement;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleSleep;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivitySampleTime;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ActivityHeartrate;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructureType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class ActivitySampleHandler extends AbstractResponseHandler {
private static final Logger logger = LoggerFactory.getLogger(ActivitySampleHandler.class);
private ActivityEntry activityEntry;
private List<ActivityEntry> activityEntries = new ArrayList<>();
private List<ActivityEntry> heartrateEntries = new ArrayList<>();
public ActivitySampleHandler(WithingsSteelHRDeviceSupport support) {
super(support);
}
@Override
public void handleResponse(Message response) {
List<WithingsStructure> data = response.getDataStructures();
if (data != null) {
handleActivityData(data, response.getType());
}
}
public void onSyncFinished() {
mergeHeartrateSamplesIntoActivitySammples();
saveData();
}
private void handleActivityData(List<WithingsStructure> dataList, short activityType) {
for (WithingsStructure data : dataList) {
switch (data.getType()) {
case WithingsStructureType.ACTIVITY_SAMPLE_TIME:
handleTimestamp(data, activityType);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_DURATION:
handleDuration(data);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_MOVEMENT:
handleMovement(data);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES:
handleCalories1(data);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2:
handleCalories2(data);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_SLEEP:
handleSleep(data);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_WALK:
handleWalk(data);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_RUN:
handleRun(data);
break;
case WithingsStructureType.ACTIVITY_SAMPLE_SWIM:
handleSwim(data);
break;
case WithingsStructureType.ACTIVITY_HR:
handleHeartrate(data);
break;
case WithingsStructureType.WORKOUT_TYPE:
handleWorkoutType(data);
break;
default:
logger.info("Received yet unhandled activity data of type '" + data.getType() + "' with data '" + GB.hexdump(data.getRawData()) + "'.");
}
}
if (activityEntry != null) {
addToList(activityEntry);
}
}
private void handleTimestamp(WithingsStructure data, short activityType) {
if (activityEntry != null) {
addToList(activityEntry);
}
activityEntry = new ActivityEntry();
activityEntry.setIsHeartrate(activityType == WithingsMessageType.GET_HEARTRATE_SAMPLES);
activityEntry.setTimestamp((int)(((ActivitySampleTime)data).getDate().getTime()/1000));
}
private void handleWorkoutType(WithingsStructure data) {
WithingsActivityType activityType = WithingsActivityType.fromCode(((WorkoutType)data).getActivityType());
activityEntry.setRawKind(activityType.toActivityKind());
}
private void handleDuration(WithingsStructure data) {
activityEntry.setDuration(((ActivitySampleDuration)data).getDuration());
}
private void handleHeartrate(WithingsStructure data) {
activityEntry.setIsHeartrate(((ActivityHeartrate)data).getHeartrate());
}
private void handleMovement(WithingsStructure data) {
activityEntry.setRawKind(ActivityKind.TYPE_UNKNOWN);
activityEntry.setSteps(((ActivitySampleMovement)data).getSteps());
activityEntry.setDistance(((ActivitySampleMovement)data).getDistance());
}
private void handleWalk(WithingsStructure data) {
activityEntry.setRawKind(ActivityKind.TYPE_WALKING);
}
private void handleRun(WithingsStructure data) {
activityEntry.setRawKind(ActivityKind.TYPE_RUNNING);
}
private void handleSwim(WithingsStructure data) {
activityEntry.setRawKind(ActivityKind.TYPE_SWIMMING);
}
private void handleSleep(WithingsStructure data) {
int sleepType;
switch (((ActivitySampleSleep)data).getSleepType()) {
case 0:
sleepType = ActivityKind.TYPE_LIGHT_SLEEP;
activityEntry.setRawIntensity(10);
break;
case 2:
sleepType = ActivityKind.TYPE_DEEP_SLEEP;
activityEntry.setRawIntensity(70);
break;
case 3:
sleepType = ActivityKind.TYPE_REM_SLEEP;
activityEntry.setRawIntensity(80);
break;
default:
sleepType = ActivityKind.TYPE_LIGHT_SLEEP;
activityEntry.setRawIntensity(50);
}
activityEntry.setRawKind(sleepType);
}
private void handleCalories1(WithingsStructure data) {
activityEntry.setRawIntensity(((ActivitySampleCalories)data).getMet());
activityEntry.setCalories(((ActivitySampleCalories)data).getCalories());
}
private void handleCalories2(WithingsStructure data) {
activityEntry.setRawIntensity(((ActivitySampleCalories2)data).getMet());
activityEntry.setCalories(((ActivitySampleCalories2)data).getCalories());
}
private void addToList(ActivityEntry activityEntry) {
if (activityEntry.isHeartrate()) {
heartrateEntries.add(activityEntry);
} else {
activityEntries.add(activityEntry);
}
}
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);
}
provider.addGBActivitySamples(activitySamples.toArray(new WithingsSteelHRActivitySample[0]));
} catch (Exception ex) {
logger.warn("Error saving activity data: " + ex.getLocalizedMessage());
}
}
private void mergeHeartrateSamplesIntoActivitySammples() {
for (ActivityEntry heartrateEntry : heartrateEntries) {
for (ActivityEntry activityEntry : activityEntries) {
if (doActivitiesOverlap(heartrateEntry, activityEntry)) {
updateHeartrateEntry(heartrateEntry, activityEntry);
}
}
}
}
private boolean doActivitiesOverlap(ActivityEntry heartrateEntry, ActivityEntry activityEntry) {
return activityEntry.getTimestamp() <= heartrateEntry.getTimestamp()
&& (activityEntry.getTimestamp() + activityEntry.getDuration()) >= heartrateEntry.getTimestamp();
}
private void updateHeartrateEntry(ActivityEntry heartRateEntry, ActivityEntry activityEntry) {
heartRateEntry.setRawKind(activityEntry.getRawKind());
heartRateEntry.setRawIntensity(activityEntry.getRawIntensity());
heartRateEntry.setDuration(activityEntry.getDuration() - (heartRateEntry.getTimestamp() - activityEntry.getTimestamp()));
// If timestamps are exactly the same and only then, the heartrate entry would overwrite the activity entry in the DB, so we set more values.
// If we would do so everytime, steps and so on would be multiplicated.
if (heartRateEntry.getTimestamp() == activityEntry.getTimestamp()) {
heartRateEntry.setSteps(activityEntry.getSteps());
heartRateEntry.setDistance(activityEntry.getDistance());
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);
}
}

View File

@ -0,0 +1,57 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
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.communication.datastructures.BatteryValues;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public class BatteryStateHandler extends AbstractResponseHandler {
public BatteryStateHandler(WithingsSteelHRDeviceSupport support) {
super(support);
}
@Override
public void handleResponse(Message response) {
handleBatteryState(response.getStructureByType(BatteryValues.class));
}
private void handleBatteryState(BatteryValues batteryValues) {
if (batteryValues == null) {
return;
}
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.level = batteryValues.getPercent();
switch (batteryValues.getStatus()) {
case 0:
batteryInfo.state = BatteryState.BATTERY_CHARGING;
break;
case 1:
batteryInfo.state = BatteryState.BATTERY_LOW;
break;
default:
batteryInfo.state = BatteryState.BATTERY_NORMAL;
}
batteryInfo.voltage = batteryValues.getVolt();
support.evaluateGBDeviceEvent(batteryInfo);
}
}

View File

@ -0,0 +1,28 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public interface Conversation {
void registerObserver(ConversationObserver observer);
void removeObserver(ConversationObserver observer);
void setRequest(Message message);
Message getRequest();
void handleResponse(Message mesage);
boolean isComplete();
}

View File

@ -0,0 +1,21 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
public interface ConversationObserver {
void onConversationCompleted(short conversationType);
}

View File

@ -0,0 +1,100 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import android.bluetooth.BluetoothGattCharacteristic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.WithingsUUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class ConversationQueue implements ConversationObserver
{
private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRDeviceSupport.class);
private final LinkedList<Conversation> queue = new LinkedList<>();
private WithingsSteelHRDeviceSupport support;
public ConversationQueue(WithingsSteelHRDeviceSupport support) {
this.support = support;
}
@Override
public void onConversationCompleted(short conversationType) {
queue.remove(getConversation(conversationType));
send();
}
public void clear() {
queue.clear();
}
public void send() {
logger.debug("Sending of queued messages has been requested.");
if (!queue.isEmpty()) {
Conversation nextInLine = queue.peek();
if (nextInLine!= null) {
logger.debug("Sending next queued message.");
Message request = nextInLine.getRequest();
support.sendToDevice(request);
}
}
}
public void addConversation(Conversation conversation) {
if (conversation == null) {
return;
}
if (conversation.getRequest().needsResponse() || conversation.getRequest().needsEOT()) {
queue.add(conversation);
conversation.registerObserver(this);
} else {
support.sendToDevice(conversation.getRequest());
}
}
public void processResponse(Message response) {
Conversation conversation = getConversation(response.getType());
if (conversation != null) {
conversation.handleResponse(response);
}
}
private Conversation getConversation(short requestType) {
for (Conversation conversation : queue) {
if (conversation.getRequest() != null && conversation.getRequest().getType() == requestType) {
return conversation;
}
}
return null;
}
}

View File

@ -0,0 +1,87 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.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.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
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;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public class HeartRateHandler extends AbstractResponseHandler {
private static final Logger logger = LoggerFactory.getLogger(HeartRateHandler.class);
public HeartRateHandler(WithingsSteelHRDeviceSupport support) {
super(support);
}
@Override
public void handleResponse(Message response) {
if (response.getDataStructures().size() > 0) {
handleHeartRateData(response.getDataStructures().get(0));
}
}
private void handleHeartRateData(WithingsStructure structure) {
int heartRate = 0;
if (structure instanceof HeartRate) {
heartRate = ((HeartRate)structure).getHeartrate();
} else if (structure instanceof LiveHeartRate) {
heartRate = ((LiveHeartRate)structure).getHeartrate();
}
if (heartRate > 0) {
saveHeartRateData(heartRate);
}
}
private void saveHeartRateData(int heartRate) {
WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample();
sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L));
sample.setHeartRate(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());
sample.setDeviceId(deviceId);
sample.setUserId(userId);
sample = SleepActivitySampleHelper.mergeIfNecessary(provider, sample);
provider.addGBActivitySample(sample);
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
LocalBroadcastManager.getInstance(support.getContext()).sendBroadcast(intent);
} catch (Exception ex) {
logger.warn("Error saving current heart rate: " + ex.getLocalizedMessage());
}
}
}

View File

@ -0,0 +1,23 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public interface ResponseHandler {
void handleResponse(Message response);
}

View File

@ -0,0 +1,35 @@
/* Copyright (C) 2021 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 <http://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.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class SetupFinishedHandler extends AbstractResponseHandler {
public SetupFinishedHandler(WithingsSteelHRDeviceSupport support) {
super(support);
}
@Override
public void handleResponse(Message response) {
if (response.getType() == WithingsMessageType.SETUP_FINISHED) {
support.finishInitialization();;
}
}
}

View File

@ -0,0 +1,42 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public class SimpleConversation extends AbstractConversation {
public SimpleConversation(ResponseHandler responseHandler) {
super(responseHandler);
}
public SimpleConversation() {
super(null);
}
@Override
protected void doSendRequest(Message message) {
// Do nothing
}
@Override
protected void doHandleResponse(Message message) {
if (responseHandler != null) {
responseHandler.handleResponse(message);
}
}
}

View File

@ -0,0 +1,37 @@
/* Copyright (C) 2021 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 <http://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.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class SyncFinishedHandler extends AbstractResponseHandler {
public SyncFinishedHandler(WithingsSteelHRDeviceSupport support) {
super(support);
}
@Override
public void handleResponse(Message response) {
if (response.getType() == WithingsMessageType.SYNC_OK) {
support.finishSync();
}
}
}

View File

@ -0,0 +1,64 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation;
import android.content.SharedPreferences;
import java.util.ArrayList;
import java.util.List;
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.activity.WithingsActivityType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutScreenList;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public class WorkoutScreenListHandler extends AbstractResponseHandler {
public WorkoutScreenListHandler(WithingsSteelHRDeviceSupport support) {
super(support);
}
@Override
public void handleResponse(Message response) {
List<WithingsStructure> data = response.getDataStructures();
if (data != null && !data.isEmpty()) {
WorkoutScreenList screenList = (WorkoutScreenList) data.get(0);
saveScreenList(screenList);
}
}
private void saveScreenList(WorkoutScreenList screenList) {
int[] workoutIds = screenList.getWorkoutIds();
List<String> prefValues = new ArrayList<>();
for (int i = 0; i < workoutIds.length; i++) {
int currentId = workoutIds[i];
if (currentId > 0) {
WithingsActivityType type = WithingsActivityType.fromCode(currentId);
prefValues.add(type.name().toLowerCase(Locale.ROOT));
}
}
String workoutActivityTypes = String.join(",", prefValues);
GBDevice device = support.getDevice();
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(device.getAddress());
prefs.edit().putString("workout_activity_types_sortable", workoutActivityTypes).apply();
}
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ActivityHeartrate extends WithingsStructure
{
private int heartrate;
public int getHeartrate() {
return heartrate;
}
@Override
public short getLength() {
return 8;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
heartrate = rawDataBuffer.get(0) & 0xff;
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_HR;
}
}

View File

@ -0,0 +1,54 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ActivitySampleCalories extends WithingsStructure {
private int calories;
private int met;
public int getCalories() {
return calories;
}
public int getMet() {
return met;
}
@Override
public short getLength() {
return 8;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
calories = rawDataBuffer.getShort() & 65535;
met = rawDataBuffer.getShort() & 65535;
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_CALORIES;
}
}

View File

@ -0,0 +1,25 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
public class ActivitySampleCalories2 extends ActivitySampleCalories {
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2;
}
}

View File

@ -0,0 +1,51 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
public class ActivitySampleDuration extends WithingsStructure {
private short duration;
public short getDuration() {
return duration;
}
@Override
public short getLength() {
return 6;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
duration = rawDataBuffer.getShort();
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_DURATION;
}
}

View File

@ -0,0 +1,69 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ActivitySampleMovement extends WithingsStructure {
private short steps;
private int distance;
private int asc;
private int desc;
public short getSteps() {
return steps;
}
public void setSteps(short steps) {
this.steps = steps;
}
public int getDistance() {
return distance;
}
public void setDistance(int distance) {
this.distance = distance;
}
@Override
public short getLength() {
return 18;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.putShort(steps);
buffer.putInt(distance);
buffer.putInt(asc);
buffer.putInt(desc);
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
steps = rawDataBuffer.getShort();
distance = rawDataBuffer.getInt();
asc = rawDataBuffer.getInt();
desc = rawDataBuffer.getInt();
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_MOVEMENT;
}
}

View File

@ -0,0 +1,36 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ActivitySampleRun extends WithingsStructure {
@Override
public short getLength() {
return 0;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_RUN;
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
public class ActivitySampleSleep extends WithingsStructure {
private short sleepType;
public short getSleepType() {
return sleepType;
}
@Override
public short getLength() {
return 6;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
sleepType = rawDataBuffer.getShort();
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_SLEEP;
}
}

View File

@ -0,0 +1,36 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ActivitySampleSwim extends WithingsStructure {
@Override
public short getLength() {
return 0;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_SWIM;
}
}

View File

@ -0,0 +1,52 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.util.Date;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
public class ActivitySampleTime extends WithingsStructure {
private Date date;
public Date getDate() {
return date;
}
@Override
public short getLength() {
return 8;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
date = new Date(timestampInSeconds * 1000);
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_TIME;
}
}

View File

@ -0,0 +1,36 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ActivitySampleUnknown extends WithingsStructure {
@Override
public short getLength() {
return 8;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_UNKNOWN;
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
public class ActivitySampleWalk extends WithingsStructure {
private short level;
public short getLevel() {
return level;
}
@Override
public short getLength() {
return 6;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
level = (short) (rawDataBuffer.getShort() & 65535);
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_SAMPLE_WALK;
}
}

View File

@ -0,0 +1,51 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ActivityTarget extends WithingsStructure {
private long targetCount;
public ActivityTarget(long targetCount) {
this.targetCount = targetCount;
}
public long getTargetCount() {
return targetCount;
}
public void setTargetCount(long targetCount) {
this.targetCount = targetCount;
}
@Override
public short getLength() {
return 12;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
rawDataBuffer.putLong(targetCount);
}
@Override
public short getType() {
return WithingsStructureType.ACTIVITY_TARGET;
}
}

View File

@ -0,0 +1,44 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class AlarmName extends WithingsStructure {
private String name;
public AlarmName(String name) {
this.name = name;
}
@Override
public short getLength() {
return (short) ((name != null ? name.getBytes().length : 0) + 1 + HEADER_SIZE);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
addStringAsBytesWithLengthByte(rawDataBuffer, name);
}
@Override
public short getType() {
return WithingsStructureType.ALARM_NAME;
}
}

View File

@ -0,0 +1,111 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class AlarmSettings extends WithingsStructure {
private short hour;
private short minute;
private short dayOfWeek;
private short dayOfMonth;
private short month;
private short year;
private short smartWakeupMinutes;
public short getHour() {
return hour;
}
public void setHour(short hour) {
this.hour = hour;
}
public short getMinute() {
return minute;
}
public void setMinute(short minute) {
this.minute = minute;
}
public short getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(short dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public short getDayOfMonth() {
return dayOfMonth;
}
public void setDayOfMonth(short dayOfMonth) {
this.dayOfMonth = dayOfMonth;
}
public short getMonth() {
return month;
}
public void setMonth(short month) {
this.month = month;
}
public short getYear() {
return year;
}
public void setYear(short year) {
this.year = year;
}
public short getYetUnkown() {
return smartWakeupMinutes;
}
public void setSmartWakeupMinutes(short smartWakeupMinutes) {
this.smartWakeupMinutes = smartWakeupMinutes;
}
@Override
public short getLength() {
return 11;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put((byte)hour);
buffer.put((byte)minute);
buffer.put((byte)dayOfWeek);
buffer.put((byte)dayOfMonth);
buffer.put((byte)month);
buffer.put((byte)year);
buffer.put((byte)smartWakeupMinutes);
}
@Override
public short getType() {
return WithingsStructureType.ALARM;
}
@Override
public boolean withEndOfMessage() {
return true;
}
}

View File

@ -0,0 +1,51 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class AlarmStatus extends WithingsStructure {
private boolean enabled;
public AlarmStatus(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
@Override
public short getLength() {
return 5;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put(enabled? (byte) 1 : (byte) 0);
}
@Override
public short getType() {
return WithingsStructureType.ALARM_STATUS;
}
}

View File

@ -0,0 +1,45 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class AncsStatus extends WithingsStructure {
private boolean isOn;
public AncsStatus() {}
public AncsStatus(boolean isOn) {
this.isOn = isOn;
}
@Override
public short getLength() {
return 5;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put(isOn? (byte)0x01 : 0x00);
}
@Override
public short getType() {
return WithingsStructureType.ANCS_STATUS;
}
}

View File

@ -0,0 +1,73 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
public class BatteryValues extends WithingsStructure {
private short percent;
private short status;
private int volt;
public short getPercent() {
return percent;
}
public void setPercent(short percent) {
this.percent = percent;
}
public short getStatus() {
return status;
}
public void setStatus(short status) {
this.status = status;
}
public int getVolt() {
return volt;
}
public void setVolt(int volt) {
this.volt = volt;
}
@Override
public short getLength() {
return 14;
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
percent = (short)(rawDataBuffer.get() & 255);
status = (short)(rawDataBuffer.get() & 255);
volt = rawDataBuffer.getInt();
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public short getType() {
return WithingsStructureType.BATTERY_STATUS;
}
}

View File

@ -0,0 +1,80 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class Challenge extends WithingsStructure {
private String macAddress;
private byte[] challenge;
public void setMacAddress(String macAddress) {
this.macAddress = macAddress;
}
public void setChallenge(byte[] challenge) {
this.challenge = challenge;
}
public String getMacAddress() {
return macAddress;
}
public byte[] getChallenge() {
return challenge;
}
@Override
public short getLength() {
int challengeLength = 0;
int macAddressLength = (macAddress != null ? macAddress.getBytes().length : 0) + 1;
if (challenge != null) {
challengeLength = challenge.length;
}
return (short) (macAddressLength + challengeLength + 5);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
addStringAsBytesWithLengthByte(buffer, macAddress);
if (challenge != null) {
buffer.put((byte) challenge.length);
buffer.put(challenge);
} else {
buffer.put((byte)0);
}
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
macAddress = getNextString(rawDataBuffer);
challenge = getNextByteArray(rawDataBuffer);
}
@Override
public short getType() {
return WithingsStructureType.CHALLENGE;
}
}

View File

@ -0,0 +1,54 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class ChallengeResponse extends WithingsStructure {
private byte[] response = new byte[0];
public byte[] getResponse() {
return response;
}
public void setResponse(byte[] response) {
this.response = response;
}
@Override
public short getLength() {
return (short) ((response != null ? response.length : 0) + 5);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
addByteArrayWithLengthByte(buffer, response);
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
response = getNextByteArray(rawDataBuffer);
}
@Override
public short getType() {
return WithingsStructureType.CHALLENGE_RESPONSE;
}
}

View File

@ -0,0 +1,168 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class DataStructureFactory {
private static final Logger logger = LoggerFactory.getLogger(DataStructureFactory.class.getSimpleName());
private static final int HEADER_SIZE = 4;
public List<WithingsStructure> createStructuresFromRawData(byte[] rawData) {
List<WithingsStructure> structures = new ArrayList<>();
if (rawData == null) {
return structures;
}
List<byte[]> rawDataStructures = splitRawData(rawData);
for (byte[] rawDataStructure : rawDataStructures) {
WithingsStructure structure = null;
short structureTypeFromResponse = (short) BLETypeConversions.toInt16(rawDataStructure[1], rawDataStructure[0]);
switch (structureTypeFromResponse) {
case WithingsStructureType.HR:
structure = new HeartRate();
break;
case WithingsStructureType.LIVE_HR:
structure = new LiveHeartRate();
break;
case WithingsStructureType.BATTERY_STATUS:
structure = new BatteryValues();
break;
case WithingsStructureType.SCREEN_SETTINGS:
structure = new ScreenSettings();
break;
case WithingsStructureType.ANCS_STATUS:
structure = new AncsStatus();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_TIME:
structure = new ActivitySampleTime();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_DURATION:
structure = new ActivitySampleDuration();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_MOVEMENT:
structure = new ActivitySampleMovement();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES:
structure = new ActivitySampleCalories();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2:
structure = new ActivitySampleCalories2();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_SLEEP:
structure = new ActivitySampleSleep();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_WALK:
structure = new ActivitySampleWalk();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_RUN:
structure = new ActivitySampleRun();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_SWIM:
structure = new ActivitySampleSwim();
break;
case WithingsStructureType.ACTIVITY_HR:
structure = new ActivityHeartrate();
break;
case WithingsStructureType.PROBE_REPLY:
structure = new ProbeReply();
break;
case WithingsStructureType.CHALLENGE:
structure = new Challenge();
break;
case WithingsStructureType.CHALLENGE_RESPONSE:
structure = new ChallengeResponse();
break;
case WithingsStructureType.ACTIVITY_SAMPLE_UNKNOWN:
structure = new ActivitySampleUnknown();
break;
case WithingsStructureType.END_OF_TRANSMISSION:
structure = new EndOfTransmission();
break;
case WithingsStructureType.WORKOUT_TYPE:
structure = new WorkoutType();
break;
case WithingsStructureType.LIVE_WORKOUT_START:
structure = new LiveWorkoutStart();
break;
case WithingsStructureType.LIVE_WORKOUT_END:
structure = new LiveWorkoutEnd();
break;
case WithingsStructureType.LIVE_WORKOUT_PAUSE_STATE:
structure = new LiveWorkoutPauseState();
break;
case WithingsStructureType.WORKOUT_SCREEN_LIST:
structure = new WorkoutScreenList();
break;
case WithingsStructureType.IMAGE_META_DATA:
structure = new ImageMetaData();
break;
case WithingsStructureType.GLYPH_ID:
structure = new GlyphId();
break;
case WithingsStructureType.NOTIFICATION_APP_ID:
structure = new GlyphId();
break;
default:
structure = null;
logger.info("Received yet unknown structure type: " + structureTypeFromResponse);
}
if (structure != null) {
structure.fillFromRawData(removeHeaderBytes(rawDataStructure));
structures.add(structure);
}
}
return structures;
}
private List<byte[]> splitRawData(byte[] rawData) {
int remainingBytes = rawData.length;
List<byte[]> result = new ArrayList<>();
while(remainingBytes > 3) {
short structureLength = (short) BLETypeConversions.toInt16(rawData[3], rawData[2]);
remainingBytes -= (structureLength + HEADER_SIZE);
try {
result.add(Arrays.copyOfRange(rawData, 0, structureLength + HEADER_SIZE));
if (remainingBytes > 0) {
rawData = Arrays.copyOfRange(rawData, structureLength + HEADER_SIZE, rawData.length);
}
} catch (Exception e) {
logger.warn("Splitting of data failed: " + GB.hexdump(rawData));
}
}
return result;
}
private byte[] removeHeaderBytes(byte[] data) {
return Arrays.copyOfRange(data, HEADER_SIZE, data.length);
}
}

View File

@ -0,0 +1,44 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class EndOfTransmission extends WithingsStructure {
@Override
public short getLength() {
return 4;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public byte[] getRawData() {
ByteBuffer rawDataBuffer = ByteBuffer.allocate(4);
rawDataBuffer.putShort(getType());
rawDataBuffer.putShort((short)0);
return rawDataBuffer.array();
}
@Override
public short getType() {
return WithingsStructureType.END_OF_TRANSMISSION;
}
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class GetActivitySamples extends WithingsStructure {
public long timestampFrom;
public short maxSampleCount;
public GetActivitySamples(long timestampFrom, short maxSampleCount) {
this.timestampFrom = timestampFrom;
this.maxSampleCount = maxSampleCount;
}
@Override
public short getLength() {
return 10;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.putInt((int)(timestampFrom & 4294967295L));
buffer.putShort((short)maxSampleCount);
}
@Override
public short getType() {
return WithingsStructureType.GET_ACTIVITY_SAMPLES;
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
public class GlyphId extends WithingsStructure {
private long unicode;
public long getUnicode() {
return unicode;
}
@Override
public short getLength() {
return 8;
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
int value = rawDataBuffer.getInt();
unicode = ByteBuffer.allocate(4).putInt(value).order(ByteOrder.LITTLE_ENDIAN).getInt(0);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public short getType() {
return WithingsStructureType.GLYPH_ID;
}
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class HeartRate extends WithingsStructure {
private int heartrate;
public int getHeartrate() {
return heartrate;
}
@Override
public short getLength() {
return 5;
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
heartrate = rawDataBuffer.get(1) & 0xff;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public short getType() {
return WithingsStructureType.HR;
}
}

View File

@ -0,0 +1,47 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ImageData extends WithingsStructure {
byte [] imageData;
public void setImageData(byte[] imageData) {
this.imageData = imageData;
}
@Override
public short getLength() {
return imageData != null ? (short)(imageData.length + 1 + HEADER_SIZE) : 1 + HEADER_SIZE;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
if (imageData != null) {
addByteArrayWithLengthByte(buffer, imageData);
} else {
addByteArrayWithLengthByte(buffer, new byte[0]);
}
}
@Override
public short getType() {
return WithingsStructureType.IMAGE_DATA;
}
}

View File

@ -0,0 +1,66 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ImageMetaData extends WithingsStructure {
private byte unknown = 0x00;
private byte width;
private byte height;
public byte getWidth() {
return width;
}
public void setWidth(byte width) {
this.width = width;
}
public byte getHeight() {
return height;
}
public void setHeight(byte height) {
this.height = height;
}
@Override
public short getLength() {
return 7;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put(unknown);
buffer.put(width);
buffer.put(height);
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
unknown = rawDataBuffer.get();
width = rawDataBuffer.get();
height = rawDataBuffer.get();
}
@Override
public short getType() {
return WithingsStructureType.IMAGE_META_DATA;
}
}

View File

@ -0,0 +1,48 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class LiveHeartRate extends WithingsStructure {
private int heartrate;
public int getHeartrate() {
return heartrate;
}
@Override
public short getLength() {
return 1;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
heartrate = rawDataBuffer.get() & 0xff;
}
@Override
public short getType() {
return WithingsStructureType.LIVE_HR;
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.util.Date;
public class LiveWorkoutEnd extends WithingsStructure {
private Date endtime;
public Date getEndtime() {
return endtime;
}
@Override
public short getLength() {
return 8;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
endtime = new Date(timestampInSeconds * 1000);
}
@Override
public short getType() {
return WithingsStructureType.LIVE_WORKOUT_END;
}
}

View File

@ -0,0 +1,66 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.util.Date;
public class LiveWorkoutPauseState extends WithingsStructure {
private byte yetunknown;
// Is always the same as long as the actual pause continues:
private Date starttime;
// This is just a guess, but observation show that this is quite possible the meaning of this value that is send when the pause is over
private int lengthInSeconds;
public Date getStarttime() {
return starttime;
}
public int getLengthInSeconds() {
return lengthInSeconds;
}
@Override
public short getLength() {
return 13;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
yetunknown = rawDataBuffer.get();
long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
if (timestampInSeconds > 0) {
starttime = new Date(timestampInSeconds * 1000);
}
lengthInSeconds = rawDataBuffer.getInt();
}
@Override
public short getType() {
return WithingsStructureType.LIVE_WORKOUT_PAUSE_STATE;
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.util.Date;
public class LiveWorkoutStart extends WithingsStructure {
private Date starttime;
public Date getStarttime() {
return starttime;
}
@Override
public short getLength() {
return 8;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
long timestampInSeconds = rawDataBuffer.getInt() & 4294967295L;
starttime = new Date(timestampInSeconds * 1000);
}
@Override
public short getType() {
return WithingsStructureType.LIVE_WORKOUT_START;
}
}

View File

@ -0,0 +1,44 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class Locale extends WithingsStructure {
private String locale = "en";
public Locale(String locale) {
this.locale = locale;
}
@Override
public short getLength() {
return (short) ((locale != null ? locale.getBytes().length : 0) + 1 + HEADER_SIZE);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
addStringAsBytesWithLengthByte(rawDataBuffer, locale);
}
@Override
public short getType() {
return WithingsStructureType.LOCALE;
}
}

View File

@ -0,0 +1,49 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class MoveHand extends WithingsStructure {
private short hand;
private short movement;
public void setHand(short hand) {
this.hand = hand;
}
public void setMovement(short movement) {
this.movement = movement;
}
@Override
public short getLength() {
return 7;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put((byte) (hand & 255));
buffer.putShort(movement);
}
@Override
public short getType() {
return WithingsStructureType.MOVE_HAND;
}
}

View File

@ -0,0 +1,51 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class Probe extends WithingsStructure {
private short os;
private short app;
private long version;
public Probe(short os, short app, long version) {
this.os = os;
this.app = app;
this.version = version;
}
@Override
public short getLength() {
return 10;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put((byte) (os & 255));
buffer.put((byte) (app & 255));
buffer.putInt((int) (version & 4294967295L));
}
@Override
public short getType() {
return WithingsStructureType.PROBE;
}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class ProbeOsVersion extends WithingsStructure {
private short osVersion;
public ProbeOsVersion(short osVersion) {
this.osVersion = osVersion;
}
@Override
public short getLength() {
return 6;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.putShort(osVersion);
}
@Override
public short getType() {
return WithingsStructureType.PROBE_OS_VERSION;
}
}

View File

@ -0,0 +1,111 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.AuthenticationHandler;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class ProbeReply extends WithingsStructure {
private static final Logger logger = LoggerFactory.getLogger(ProbeReply.class);
private int yetUnknown1;
private String name;
private String mac;
private String secret;
private int yetUnknown2;
private String mId;
private int yetUnknown3;
private int firmwareVersion;
private int yetUnknown4;
public int getYetUnknown1() {
return yetUnknown1;
}
public String getName() {
return name;
}
public String getMac() {
return mac;
}
public String getSecret() {
return secret;
}
public int getYetUnknown2() {
return yetUnknown2;
}
public String getmId() {
return mId;
}
public int getYetUnknown3() {
return yetUnknown3;
}
public int getFirmwareVersion() {
return firmwareVersion;
}
public int getYetUnknown4() {
return yetUnknown4;
}
@Override
public short getLength() {
int length = (name != null ? name.getBytes(StandardCharsets.UTF_8).length : 0) + 1;
length += (mac != null ? mac.getBytes().length : 0) + 1;
length += (secret != null ? secret.getBytes().length : 0) + 1;
length += (mId != null ? mId.getBytes().length : 0) + 1;
return (short) (length + 24);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
try {
yetUnknown1 = rawDataBuffer.getInt();
name = getNextString(rawDataBuffer);
mac = getNextString(rawDataBuffer);
secret = getNextString(rawDataBuffer);
yetUnknown2 = rawDataBuffer.getInt();
mId = getNextString(rawDataBuffer);
yetUnknown3 = rawDataBuffer.getInt();
firmwareVersion = rawDataBuffer.getInt();
yetUnknown4 = rawDataBuffer.getInt();
} catch (Exception e) {
logger.warn("Could not handle buffer with data " + StringUtils.bytesToHex(rawDataBuffer.array()));
}
}
@Override
public short getType() {
return WithingsStructureType.PROBE_REPLY;
}
}

View File

@ -0,0 +1,67 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
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;
private byte idOnDevice;
private byte yetUnkown3 = 0x01;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public byte getIdOnDevice() {
return idOnDevice;
}
public void setIdOnDevice(byte idOnDevice) {
this.idOnDevice = idOnDevice;
}
@Override
public short getLength() {
return 22;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.putInt(this.id);
buffer.putInt(this.userId);
buffer.putInt(this.yetUnknown1);
buffer.putInt(this.yetUnknown2);
buffer.put(this.idOnDevice);
buffer.put(this.yetUnkown3);
}
@Override
public short getType() {
return WithingsStructureType.SCREEN_SETTINGS;
}
}

View File

@ -0,0 +1,52 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class SourceAppId extends WithingsStructure {
private String appId;
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
@Override
public short getLength() {
return (short) ((appId != null ? appId.getBytes().length : 0) + 5);
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
appId = getNextString(rawDataBuffer);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
addStringAsBytesWithLengthByte(buffer, appId);
}
@Override
public short getType() {
return WithingsStructureType.NOTIFICATION_APP_ID;
}
}

View File

@ -0,0 +1,36 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class Status extends WithingsStructure {
@Override
public short getLength() {
return 5;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put((byte) 0x01);
}
@Override
public short getType() {
return (short) 2420;
}
}

View File

@ -0,0 +1,102 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import org.threeten.bp.Instant;
import org.threeten.bp.ZoneId;
import org.threeten.bp.zone.ZoneOffsetTransition;
import org.threeten.bp.zone.ZoneRules;
import java.nio.ByteBuffer;
import java.util.Date;
import java.util.TimeZone;
import ch.qos.logback.core.encoder.ByteArrayUtil;
public class Time extends WithingsStructure {
private Instant now;
private int timeOffsetInSeconds;
private Instant nextDaylightSavingTransition;
private int nextDaylightSavingTransitionOffsetInSeconds;
public Time() {
now = Instant.now();
final TimeZone timezone = TimeZone.getDefault();
timeOffsetInSeconds = timezone.getOffset(now.toEpochMilli()) / 1000;
final ZoneId zoneId = ZoneId.systemDefault();
final ZoneRules zoneRules = zoneId.getRules();
final ZoneOffsetTransition nextTransition = zoneRules.nextTransition(Instant.now());
long nextTransitionTs = 0;
if (nextTransition != null) {
nextTransitionTs = nextTransition.getDateTimeBefore().atZone(zoneId).toEpochSecond();
nextDaylightSavingTransitionOffsetInSeconds = nextTransition.getOffsetAfter().getTotalSeconds();
}
nextDaylightSavingTransition = Instant.ofEpochSecond(nextTransitionTs);
}
public Instant getNow() {
return now;
}
public void setNow(Instant now) {
this.now = now;
}
public int getTimeOffsetInSeconds() {
return timeOffsetInSeconds;
}
public void setTimeOffsetInSeconds(int TimeOffsetInSeconds) {
this.timeOffsetInSeconds = TimeOffsetInSeconds;
}
public Instant getNextDaylightSavingTransition() {
return nextDaylightSavingTransition;
}
public void setNextDaylightSavingTransition(Instant nextDaylightSavingTransition) {
this.nextDaylightSavingTransition = nextDaylightSavingTransition;
}
public int getNextDaylightSavingTransitionOffsetInSeconds() {
return nextDaylightSavingTransitionOffsetInSeconds;
}
public void setNextDaylightSavingTransitionOffsetInSeconds(int nextDaylightSavingTransitionOffsetInSeconds) {
this.nextDaylightSavingTransitionOffsetInSeconds = nextDaylightSavingTransitionOffsetInSeconds;
}
@Override
public short getType() {
return WithingsStructureType.TIME;
}
@Override
public short getLength() {
return 20;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
rawDataBuffer.putInt((int)now.getEpochSecond());
rawDataBuffer.putInt(timeOffsetInSeconds);
rawDataBuffer.putInt((int)nextDaylightSavingTransition.getEpochSecond());
rawDataBuffer.putInt(nextDaylightSavingTransitionOffsetInSeconds);
}
}

View File

@ -0,0 +1,39 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class TypeVersion extends WithingsStructure {
private byte version = 0x01;
@Override
public short getLength() {
return 5;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put(version);
}
@Override
public short getType() {
return 2401;
}
}

View File

@ -0,0 +1,102 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Date;
public class User extends WithingsStructure {
// This is just a dummy value as this seems to be the withings account id,
// which we do not need, but the watch expects:
private int userID = 123456;
private int weight;
private int height;
//Seems to be 0x00 for male and 0x01 for female. Found no other in my tests.
private byte gender;
private Date birthdate;
private String name;
public int getUserID() {
return userID;
}
public void setUserID(int userID) {
this.userID = userID;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = weight;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public byte getGender() {
return gender;
}
public void setGender(byte gender) {
this.gender = gender;
}
public Date getBirthdate() {
return birthdate;
}
public void setBirthdate(Date birthdate) {
this.birthdate = birthdate;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public short getLength() {
return (short) ((name != null ? name.getBytes(StandardCharsets.UTF_8).length : 0) + 22);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
rawDataBuffer.putInt(userID);
rawDataBuffer.putInt(weight);
rawDataBuffer.putInt(height);
rawDataBuffer.put(gender);
rawDataBuffer.putInt((int)(birthdate.getTime()/1000));
addStringAsBytesWithLengthByte(rawDataBuffer, name);
}
@Override
public short getType() {
return WithingsStructureType.USER;
}
}

View File

@ -0,0 +1,39 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class UserSecret extends WithingsStructure {
private String secret = "2EM5zNP37QzM00hmP6BFTD92nG15XwNd";
@Override
public short getLength() {
return 37;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
addStringAsBytesWithLengthByte(buffer, secret);
}
@Override
public short getType() {
return WithingsStructureType.USER_SECRET;
}
}

View File

@ -0,0 +1,52 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class UserUnit extends WithingsStructure {
private byte unknown1 = 0;
private byte unknown2 = 0;
private short type;
private short unit;
public UserUnit() {}
public UserUnit(short type, short unit) {
this.type = type;
this.unit = unit;
}
@Override
public short getLength() {
return 10;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.put(unknown1);
buffer.put(unknown2);
buffer.putShort(type);
buffer.putShort(unit);
}
@Override
public short getType() {
return WithingsStructureType.USER_UNIT;
}
}

View File

@ -0,0 +1,29 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
public final class UserUnitConstants {
public static final short DISTANCE = 2;
public static final short CLOCK_MODE = 4;
public static final short UNIT_KM = 24;
public static final short UNIT_MILES = 25;
public static final short UNIT_24H = 26;
public static final short UNIT_12H = 27;
private UserUnitConstants() {}
}

View File

@ -0,0 +1,103 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
/**
* This abstract class is the common denominator for all data structures used inside commands and the corresponding responses.
* @see Message
*/
public abstract class WithingsStructure {
protected final static short HEADER_SIZE = 4;
/**
* Some messages have some end bytes, some have not.
* Subclasses that need to have the eom appended need to overwrite this class and return true.
* The default value is false.
*
* @return true if some end of message should be appended
*/
public boolean withEndOfMessage() {
return false;
}
public byte[] getRawData() {
short length = (getLength());
ByteBuffer rawDataBuffer = ByteBuffer.allocate(length);
rawDataBuffer.putShort(getType());
rawDataBuffer.putShort((short)(length - HEADER_SIZE));
fillinTypeSpecificData(rawDataBuffer);
return rawDataBuffer.array();
}
protected void addStringAsBytesWithLengthByte(ByteBuffer buffer, String str) {
if (str == null) {
buffer.put((byte)0);
} else {
byte[] stringAsBytes = str.getBytes(StandardCharsets.UTF_8);
buffer.put((byte)stringAsBytes.length);
buffer.put(stringAsBytes);
}
}
protected void fillFromRawData(byte[] rawData) {
fillFromRawDataAsBuffer(ByteBuffer.wrap(rawData));
};
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {};
public abstract short getLength();
protected abstract void fillinTypeSpecificData(ByteBuffer buffer);
public abstract short getType();
protected String getNextString(ByteBuffer byteBuffer) {
// For strings in the raw data the first byte of the data is the length of the string:
int stringLength = (short)(byteBuffer.get() & 255);
byte[] stringBytes = new byte[stringLength];
byteBuffer.get(stringBytes);
return new String(stringBytes, Charset.forName("UTF-8"));
}
protected byte[] getNextByteArray(ByteBuffer byteBuffer) {
int arrayLength = (short)(byteBuffer.get() & 255);
byte[] nextByteArray = new byte[arrayLength];
byteBuffer.get(nextByteArray);
return nextByteArray;
}
protected int[] getNextIntArray(ByteBuffer byteBuffer) {
int arrayLength = (short)(byteBuffer.get() & 255);
int[] nextIntArray = new int[arrayLength];
for (int i = 0; i < arrayLength; i++) {
nextIntArray[i] = byteBuffer.getInt();
}
return nextIntArray;
}
protected void addByteArrayWithLengthByte(ByteBuffer buffer, byte[] data) {
buffer.put((byte) data.length);
if (data.length != 0) {
buffer.put(data);
}
}
}

View File

@ -0,0 +1,72 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
public class WithingsStructureType {
public static final short END_OF_TRANSMISSION = 256;
public static final short PROBE_REPLY = 257;
public static final short PROBE = 298;
public static final short CHALLENGE = 290;
public static final short CHALLENGE_RESPONSE = 291;
public static final short PROBE_OS_VERSION = 2344;
public static final short TIME = 1281;
public static final short SCREEN_SETTINGS = 1302;
public static final short WORKOUT_SCREEN_SETTINGS = 317;
public static final short BATTERY_STATUS = 1284;
public static final short USER = 1283;
public static final short USER_SECRET = 1299;
public static final short USER_UNIT = 281;
public static final short ACTIVITY_TARGET = 1297;
public static final short LOCALE = 289;
public static final short LIVE_HR = 2369;
public static final short HR = 2343;
public static final short ACTIVITY_HR = 2345;
public static final short ALARM = 1298;
public static final short ALARM_STATUS = 2329;
public static final short ALARM_NAME = 1300;
public static final short STEPS = 2390;
public static final short IMAGE_META_DATA = 2397;
public static final short IMAGE_DATA = 2398;
public static final short ANCS_STATUS = 2346;
public static final short NOTIFICATION_APP_ID = 2404;
public static final short GLYPH_ID = 2396;
public static final short MOVE_HAND = 1292;
public static final short GET_ACTIVITY_SAMPLES = 1286;
public static final short ACTIVITY_SAMPLE_TIME = 1537;
public static final short ACTIVITY_SAMPLE_DURATION = 1538;
public static final short ACTIVITY_SAMPLE_MOVEMENT = 1539;
public static final short ACTIVITY_SAMPLE_WALK = 1540;
public static final short ACTIVITY_SAMPLE_RUN = 1541;
public static final short ACTIVITY_SAMPLE_SWIM = 1549;
public static final short ACTIVITY_SAMPLE_SLEEP = 1543;
// There are two structure types containing information about calories:
public static final short ACTIVITY_SAMPLE_CALORIES = 1544;
public static final short ACTIVITY_SAMPLE_CALORIES_2 = 1546;
// No idea what this is, however it is in the response to requesting activities:
public static final short ACTIVITY_SAMPLE_UNKNOWN = 1547;
public static final short WORKOUT_TYPE = 2409;
public static final short LIVE_WORKOUT_START = 2418;
public static final short LIVE_WORKOUT_END = 2419;
public static final short LIVE_WORKOUT_PAUSE_STATE = 2439;
public static final short WORKOUT_GPS_STATE = 321;
public static final short WORKOUT_SCREEN_LIST = 316;
public static final short WORKOUT_SCREEN_DATA = 317;
private WithingsStructureType() {}
}

View File

@ -0,0 +1,43 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class WorkoutGpsState extends WithingsStructure {
private final boolean gpsEnabled;
public WorkoutGpsState(boolean gpsEnabled) {
this.gpsEnabled = gpsEnabled;
}
@Override
public short getLength() {
return 6;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
buffer.putShort(gpsEnabled? (short)1 : 0);
}
@Override
public short getType() {
return WithingsStructureType.WORKOUT_GPS_STATE;
}
}

View File

@ -0,0 +1,67 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class WorkoutScreen extends WithingsStructure {
public static final byte MODE_PACE = 2;
public static final byte MODE_SPEED = 3;
public static final byte MODE_ELSE = 1;
public int id;
public byte yetunknown1 = 0;
public String name;
public byte mode = MODE_PACE;
public short yetunknown2 = 0;
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setMode(byte mode) {
this.mode = mode;
}
@Override
public short getType() {
return WithingsStructureType.WORKOUT_SCREEN_SETTINGS;
}
@Override
public short getLength() {
return (short) ((name != null ? name.getBytes().length : 0) + 9 + HEADER_SIZE);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer rawDataBuffer) {
rawDataBuffer.putInt(id);
rawDataBuffer.put(yetunknown1);
addStringAsBytesWithLengthByte(rawDataBuffer, name);
rawDataBuffer.put(mode);
rawDataBuffer.putShort(yetunknown2);
}
}

View File

@ -0,0 +1,52 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class WorkoutScreenData extends WithingsStructure {
public long id;
public short version;
public String name;
public short faceMode;
public int flag;
@Override
public short getLength() {
return (short) ((name != null ? name.getBytes().length : 0) + 13);
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
this.id = rawDataBuffer.getInt() & 4294967295L;
this.version = (short) (rawDataBuffer.get() & 255);
this.name = getNextString(rawDataBuffer);
this.faceMode = (short) (rawDataBuffer.get() & 255);
this.flag = rawDataBuffer.getShort() & 65535;
}
@Override
public short getType() {
return WithingsStructureType.WORKOUT_SCREEN_DATA;
}
}

View File

@ -0,0 +1,48 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class WorkoutScreenList extends WithingsStructure {
private int[] workoutIds;
public int[] getWorkoutIds() {
return workoutIds;
}
@Override
public short getLength() {
return 37;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
protected void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
workoutIds = getNextIntArray(rawDataBuffer);
}
@Override
public short getType() {
return WithingsStructureType.WORKOUT_SCREEN_LIST;
}
}

View File

@ -0,0 +1,50 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures;
import java.nio.ByteBuffer;
public class WorkoutType extends WithingsStructure {
public static short RUNNING = 0;
private short activityType;
public short getActivityType() {
return activityType;
}
@Override
public short getLength() {
return 6;
}
@Override
protected void fillinTypeSpecificData(ByteBuffer buffer) {
}
@Override
public void fillFromRawDataAsBuffer(ByteBuffer rawDataBuffer) {
activityType = rawDataBuffer.getShort();
}
@Override
public short getType() {
return WithingsStructureType.WORKOUT_TYPE;
}
}

View File

@ -0,0 +1,98 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public abstract class AbstractMessage implements Message {
/**
* The header consist of the first byte 0x01 (probably the message format identifier),
* two bytes for the message type and 2 bytes for the actual datalength.
*/
private static final int HEADER_SIZE = 5;
protected final static short EOM_SIZE = 4;
private List<WithingsStructure> dataStructures = new ArrayList<WithingsStructure>();
public List<WithingsStructure> getDataStructures() {
return Collections.unmodifiableList(dataStructures);
}
@Override
public void addDataStructure(WithingsStructure data) {
dataStructures.add(data);
}
@Override
public byte[] getRawData() {
short structureLength = 0;
boolean setEndOfMessage = false;
for (WithingsStructure structure : dataStructures) {
if (structure.withEndOfMessage()) {
setEndOfMessage = true;
}
structureLength += (short)(structure.getLength());
}
if (setEndOfMessage) {
structureLength += EOM_SIZE;
}
ByteBuffer rawDataBuffer = ByteBuffer.allocate(HEADER_SIZE + structureLength);
rawDataBuffer.put((byte)0x01); // <= This seems to be always 0x01 for all commands
rawDataBuffer.putShort(getType());
rawDataBuffer.putShort(structureLength);
for (WithingsStructure structure : dataStructures) {
rawDataBuffer.put(structure.getRawData());
}
if (setEndOfMessage) {
addEndOfMessageBytes(rawDataBuffer);
}
return rawDataBuffer.array();
}
@Override
public <T extends WithingsStructure> T getStructureByType(Class<T> type) {
for (WithingsStructure structure : this.getDataStructures()) {
if (type.isInstance(structure)) {
return (T)structure;
}
}
return null;
}
private void addEndOfMessageBytes(ByteBuffer buffer) {
buffer.putShort((short)256);
buffer.putShort((short)0);
}
public String toString() {
return GB.hexdump(this.getRawData()).toLowerCase(Locale.ROOT);
}
}

View File

@ -0,0 +1,23 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
public enum ExpectedResponse {
NONE,
SIMPLE,
EOT
}

View File

@ -0,0 +1,92 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.widget.Toast;
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.communication.datastructures.GlyphId;
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.message.incoming.IncomingMessageHandler;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class GlyphRequestHandler implements IncomingMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(GlyphRequestHandler.class);
private final WithingsSteelHRDeviceSupport support;
public GlyphRequestHandler(WithingsSteelHRDeviceSupport support) {
this.support = support;
}
@Override
public void handleMessage(Message message) {
try {
GlyphId glyphId = message.getStructureByType(GlyphId.class);
ImageMetaData imageMetaData = message.getStructureByType(ImageMetaData.class);
Message reply = new WithingsMessage(WithingsMessageType.GET_UNICODE_GLYPH);
reply.addDataStructure(glyphId);
reply.addDataStructure(imageMetaData);
ImageData imageData = new ImageData();
imageData.setImageData(createUnicodeImage(glyphId.getUnicode(), imageMetaData));
reply.addDataStructure(imageData);
logger.info("Sending reply to glyph request: " + reply);
support.sendToDevice(reply);
} catch (Exception e) {
logger.error("Failed to respond to glyph request.", e);
GB.toast("Failed to respond to glyph request:" + e.getMessage(), Toast.LENGTH_LONG, GB.WARN);
}
}
private byte[] createUnicodeImage(long unicode, ImageMetaData metaData) {
String str = new String(Character.toChars((int)unicode));
Paint paint = new Paint();
paint.setTypeface(null);
Rect rect = new Rect();
paint.setTextSize(calculateTextsize(paint, metaData.getHeight()));
paint.setAntiAlias(true);
paint.getTextBounds(str, 0, str.length(), rect);
paint.setColor(-1);
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
int width = rect.width();
if (width <= 0) {
return new byte[0];
}
Bitmap createBitmap = Bitmap.createBitmap(width, metaData.getHeight(), Bitmap.Config.ARGB_8888);
new Canvas(createBitmap).drawText(str, -rect.left, -fontMetricsInt.top, paint);
return IconHelper.toByteArray(createBitmap);
}
private int calculateTextsize(Paint paint, int height) {
Paint.FontMetricsInt fontMetricsInt;
int textsize = 0;
do {
textsize++;
paint.setTextSize(textsize);
fontMetricsInt = paint.getFontMetricsInt();
} while (fontMetricsInt.bottom - fontMetricsInt.top < height);
return textsize - 1;
}
}

View File

@ -0,0 +1,36 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
/**
* This interface is the common denominator for all messages passed to and from the Steel HR.
*
*/
public interface Message {
List<WithingsStructure> getDataStructures();
void addDataStructure(WithingsStructure data);
short getType();
byte[] getRawData();
boolean needsResponse();
boolean needsEOT();
boolean isIncomingMessage();
<T extends WithingsStructure> T getStructureByType(Class<T> type);
}

View File

@ -0,0 +1,89 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
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.util.StringUtils;
public class MessageBuilder {
private static final Logger logger = LoggerFactory.getLogger(MessageBuilder.class);
private WithingsSteelHRDeviceSupport support;
private MessageFactory messageFactory;
private ByteArrayOutputStream pendingMessage;
private Message message;
public MessageBuilder(WithingsSteelHRDeviceSupport support, MessageFactory messageFactory) {
this.support = support;
this.messageFactory = messageFactory;
}
public synchronized boolean buildMessage(byte[] rawData) {
if (pendingMessage == null && rawData[0] == 0x01) {
pendingMessage = new ByteArrayOutputStream();
} else if (pendingMessage == null) {
return false;
}
try {
pendingMessage.write(rawData);
} catch(IOException e) {
logger.error("Could not write data to stream: " + StringUtils.bytesToHex(rawData));
return false;
}
if (!isMessageComplete(pendingMessage.toByteArray())) {
return false;
} else {
Message message = messageFactory.createMessageFromRawData(pendingMessage.toByteArray());
pendingMessage = null;
if (message == null) {
logger.info("Cannot handle null message");
return false;
}
this.message = message;
return true;
}
}
public Message getMessage() {
return message;
}
private boolean isMessageComplete(byte[] messageData) {
if (messageData.length < 5) {
return false;
}
short totalDataLength = (short) BLETypeConversions.toInt16(messageData[4], messageData[3]);
byte[] rawStructureData = Arrays.copyOfRange(messageData, 5, messageData.length);
if (rawStructureData.length == totalDataLength) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,55 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.DataStructureFactory;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
public class MessageFactory {
private static final Logger logger = LoggerFactory.getLogger(MessageFactory.class);
private DataStructureFactory dataStructureFactory;
public MessageFactory(DataStructureFactory dataStructureFactory) {
this.dataStructureFactory = new DataStructureFactory();
}
public Message createMessageFromRawData(byte[] rawData) {
if (rawData.length < 5 || rawData[0] != 0x01) {
return null;
}
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;
Message message = new WithingsMessage(messageTypeFromResponse, isIncoming);
byte[] rawStructureData = Arrays.copyOfRange(rawData, 5, rawData.length);
List<WithingsStructure> structures = dataStructureFactory.createStructuresFromRawData(rawStructureData);
for (WithingsStructure structure : structures) {
message.addDataStructure(structure);
}
return message;
}
}

View File

@ -0,0 +1,71 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
import java.nio.ByteBuffer;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class SimpleHexToByteMessage implements Message {
private String hexString;
public SimpleHexToByteMessage(String hexString) {
this.hexString = hexString;
}
@Override
public List<WithingsStructure> getDataStructures() {
return null;
}
@Override
public void addDataStructure(WithingsStructure data) {
}
@Override
public short getType() {
return 0;
}
@Override
public byte[] getRawData() {
return GB.hexStringToByteArray(hexString);
}
@Override
public boolean needsResponse() {
return false;
}
@Override
public boolean needsEOT() {
return false;
}
@Override
public boolean isIncomingMessage() {
return false;
}
@Override
public <T extends WithingsStructure> T getStructureByType(Class<T> type) {
return null;
}
}

View File

@ -0,0 +1,64 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
public class WithingsMessage extends AbstractMessage {
private short type;
private ExpectedResponse expectedResponse = ExpectedResponse.SIMPLE;
private boolean isIncoming;
public WithingsMessage(short type) {
this.type = type;
}
public WithingsMessage(short type, boolean incoming) {
this.type = type;
this.isIncoming = incoming;
}
public WithingsMessage(short type, ExpectedResponse expectedResponse) {
this.type = type;
this.expectedResponse = expectedResponse;
}
public WithingsMessage(short type, WithingsStructure dataStructure) {
this.type = type;
this.addDataStructure(dataStructure);
}
@Override
public boolean needsResponse() {
return expectedResponse == ExpectedResponse.SIMPLE;
}
@Override
public boolean needsEOT() {
return expectedResponse == ExpectedResponse.EOT;
}
@Override
public short getType() {
return type;
}
@Override
public boolean isIncomingMessage() {
return isIncoming;
}
}

View File

@ -0,0 +1,68 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message;
/**
* Contains all identified commandtypes in the used TLV format of the messages exchanged
* between device and app.
*/
public final class WithingsMessageType {
public static final short PROBE = 257;
public static final short CHALLENGE = 296;
public static final short SET_TIME = 1281;
public static final short GET_BATTERY_STATUS = 1284;
public static final short SET_SCREEN_LIST = 1292;
public static final short INITIAL_CONNECT = 273;
public static final short START_HANDS_CALIBRATION = 286;
public static final short STOP_HANDS_CALIBRATION = 287;
public static final short MOVE_HAND = 284;
public static final short SET_ACTIVITY_TARGET = 1290;
public static final short SET_USER = 1282;
public static final short GET_USER = 1283;
public static final short SET_USER_UNIT = 274;
public static final short SET_LOCALE = 282;
public static final short SETUP_FINISHED = 275;
public static final short GET_HR = 2343;
public static final short GET_WORKOUT_SCREEN_LIST = 315;
public static final short SET_WORKOUT_SCREEN = 316;
public static final short START_LIVE_WORKOUT = 317;
public static final short STOP_LIVE_WORKOUT = 318;
public static final short SYNC = 321;
public static final short SYNC_RESPONSE = 16705;
public static final short SYNC_OK = 277;
public static final short GET_ALARM_SETTINGS = 298;
public static final short SET_ALARM = 325;
public static final short GET_ALARM = 293;
public static final short GET_ALARM_ENABLED = 2330;
public static final short SET_ALARM_ENABLED = 2331;
public static final short GET_ANCS_STATUS = 2353;
public static final short SET_ANCS_STATUS = 2345;
public static final short GET_SCREEN_SETTINGS = 1293;
// The next two do nearly the same, when I look at the responses, though only the first seems to deliver sleep samples
public static final short GET_ACTIVITY_SAMPLES = 2424;
public static final short GET_MOVEMENT_SAMPLES = 1286;
public static final short GET_SPORT_MODE = 2371;
public static final short GET_WORKOUT_GPS_STATUS = 323;
public static final short GET_HEARTRATE_SAMPLES = 2344;
public static final short LIVE_WORKOUT_DATA = 320;
public static final short GET_NOTIFICATION = 2404;
public static final short GET_UNICODE_GLYPH = 2403;
private WithingsMessageType() {}
}

View File

@ -0,0 +1,23 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public interface IncomingMessageHandler {
public void handleMessage(Message message);
}

View File

@ -0,0 +1,86 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
import org.slf4j.Logger;
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.communication.message.GlyphRequestHandler;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class IncomingMessageHandlerFactory {
private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandlerFactory.class);
private static IncomingMessageHandlerFactory instance;
private final WithingsSteelHRDeviceSupport support;
private Map<Short, IncomingMessageHandler> handlers = new HashMap<>();
private IncomingMessageHandlerFactory(WithingsSteelHRDeviceSupport support) {
this.support = support;
}
public static IncomingMessageHandlerFactory getInstance(WithingsSteelHRDeviceSupport support) {
if (instance == null) {
instance = new IncomingMessageHandlerFactory(support);
}
return instance;
}
public IncomingMessageHandler getHandler(Message message) {
IncomingMessageHandler handler = handlers.get(message.getType());
switch (message.getType()) {
case WithingsMessageType.START_LIVE_WORKOUT:
case WithingsMessageType.STOP_LIVE_WORKOUT:
case WithingsMessageType.GET_WORKOUT_GPS_STATUS:
if (handler == null) {
handlers.put(message.getType(), new LiveWorkoutHandler(support));
}
break;
case WithingsMessageType.LIVE_WORKOUT_DATA:
if (handler == null) {
handlers.put(message.getType(), new LiveHeartrateHandler(support));
}
break;
case WithingsMessageType.GET_NOTIFICATION:
if (handler == null) {
handlers.put(message.getType(), new NotificationRequestHandler(support));
}
break;
case WithingsMessageType.GET_UNICODE_GLYPH:
if (handler == null) {
handlers.put(message.getType(), new GlyphRequestHandler(support));
}
break;
case WithingsMessageType.SYNC:
if (handler == null) {
handlers.put(message.getType(), new SyncRequestHandler(support));
}
break;
default:
logger.warn("Unhandled incoming message type: " + message.getType());
}
return handlers.get(message.getType());
}
}

View File

@ -0,0 +1,88 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
import android.content.Intent;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.GregorianCalendar;
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.model.DeviceService;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport;
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;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
public class LiveHeartrateHandler implements IncomingMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(LiveHeartrateHandler.class);
private final WithingsSteelHRDeviceSupport support;
public LiveHeartrateHandler(WithingsSteelHRDeviceSupport support) {
this.support = support;
}
@Override
public void handleMessage(Message message) {
List<WithingsStructure> data = message.getDataStructures();
if (data == null || data.isEmpty()) {
return;
}
WithingsStructure structure = data.get(0);
int heartRate = 0;
if (structure instanceof LiveHeartRate) {
heartRate = ((LiveHeartRate)structure).getHeartrate();
}
if (heartRate > 0) {
saveHeartRateData(heartRate);
}
}
private void saveHeartRateData(int heartRate) {
WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample();
sample.setTimestamp((int) (GregorianCalendar.getInstance().getTimeInMillis() / 1000L));
sample.setHeartRate(heartRate);
try (DBHandler dbHandler = GBApplication.acquireDB()) {
Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId();
Long deviceId = DBHelper.getDevice(support.getDevice(), dbHandler.getDaoSession()).getId();
WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(support.getDevice(), dbHandler.getDaoSession());
sample.setDeviceId(deviceId);
sample.setUserId(userId);
sample = SleepActivitySampleHelper.mergeIfNecessary(provider, sample);
provider.addGBActivitySample(sample);
} catch (Exception ex) {
logger.warn("Error saving current heart rate: " + ex.getLocalizedMessage());
}
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
LocalBroadcastManager.getInstance(support.getContext()).sendBroadcast(intent);
}
}

View File

@ -0,0 +1,147 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
import android.location.LocationManager;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.entities.BaseActivitySummary;
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.activity.WithingsActivityType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutEnd;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutPauseState;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.LiveWorkoutStart;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructure;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WithingsStructureType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutGpsState;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.WorkoutType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
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 BaseActivitySummary baseActivitySummary;
public LiveWorkoutHandler(WithingsSteelHRDeviceSupport support) {
this.support = support;
}
public void handleMessage(Message message) {
List<WithingsStructure> data = message.getDataStructures();
if (data != null) {
handleLiveData(data);
}
}
private void handleLiveData(List<WithingsStructure> dataList) {
for (WithingsStructure data : dataList) {
switch (data.getType()) {
case WithingsStructureType.LIVE_WORKOUT_START:
handleStart((LiveWorkoutStart) data);
break;
case WithingsStructureType.LIVE_WORKOUT_END:
handleEnd((LiveWorkoutEnd) data);
break;
case WithingsStructureType.LIVE_WORKOUT_PAUSE_STATE:
handlePause((LiveWorkoutPauseState) data);
break;
case WithingsStructureType.WORKOUT_TYPE:
handleType((WorkoutType) data);
break;
default:
logger.info("Received yet unhandled live workout data of type '" + data.getType() + "' with data '" + GB.hexdump(data.getRawData()) + "'.");
}
}
}
private void handleStart(LiveWorkoutStart workoutStart) {
sendGpsState();
if (baseActivitySummary == null) {
baseActivitySummary = new BaseActivitySummary();
}
baseActivitySummary.setStartTime(workoutStart.getStarttime());
}
private void handlePause(LiveWorkoutPauseState workoutPause) {
// Not sure what to do with these events at the moment so we just log them.
if (workoutPause.getStarttime() == null) {
if (workoutPause.getLengthInSeconds() > 0) {
logger.info("Got workout pause end with duration: " + workoutPause.getLengthInSeconds());
} else {
logger.info("Currently no pause happened");
}
} else {
logger.info("Got workout pause started at: " + workoutPause.getStarttime());
}
}
private void handleEnd(LiveWorkoutEnd workoutEnd) {
OpenTracksController.stopRecording(support.getContext());
baseActivitySummary.setEndTime(workoutEnd.getEndtime());
saveBaseActivitySummary();
baseActivitySummary = null;
}
private void handleType(WorkoutType workoutType) {
WithingsActivityType withingsWorkoutType = WithingsActivityType.fromCode(workoutType.getActivityType());
OpenTracksController.startRecording(support.getContext(), withingsWorkoutType.toActivityKind());
if (baseActivitySummary == null) {
baseActivitySummary = new BaseActivitySummary();
}
baseActivitySummary.setActivityKind(withingsWorkoutType.toActivityKind());
}
private void sendGpsState() {
Message message = new WithingsMessage((short)(WithingsMessageType.START_LIVE_WORKOUT | 16384), new WorkoutGpsState(isGpsEnabled()));
support.sendToDevice(message);
}
private boolean isGpsEnabled() {
final LocationManager manager = (LocationManager) support.getContext().getSystemService(support.getContext().LOCATION_SERVICE );
return manager.isProviderEnabled(LocationManager.GPS_PROVIDER );
}
private void saveBaseActivitySummary() {
try (DBHandler dbHandler = GBApplication.acquireDB()) {
DaoSession session = dbHandler.getDaoSession();
Device device = DBHelper.getDevice(support.getDevice(), session);
User user = DBHelper.getUser(session);
baseActivitySummary.setDevice(device);
baseActivitySummary.setUser(user);
session.getBaseActivitySummaryDao().insertOrReplace(baseActivitySummary);
} catch (Exception ex) {
GB.toast(support.getContext(), "Error saving activity summary", Toast.LENGTH_LONG, GB.ERROR, ex);
}
}
}

View File

@ -0,0 +1,101 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.incoming;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.widget.Toast;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
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.communication.datastructures.ImageData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.ImageMetaData;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures.SourceAppId;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
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.notification.NotificationProvider;
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 Map<String, byte[]> appIconCache = new HashMap<>();
public NotificationRequestHandler(WithingsSteelHRDeviceSupport support) {
this.support = support;
}
@Override
public void handleMessage(Message message) {
try {
SourceAppId appId = message.getStructureByType(SourceAppId.class);
ImageMetaData imageMetaData = message.getStructureByType(ImageMetaData.class);
Message reply = new WithingsMessage(WithingsMessageType.GET_NOTIFICATION);
reply.addDataStructure(appId);
reply.addDataStructure(imageMetaData);
ImageData imageData = new ImageData();
imageData.setImageData(getImageData(appId.getAppId()));
reply.addDataStructure(imageData);
logger.info("Sending reply to notification request: " + reply);
support.sendToDevice(reply);
} catch (Exception e) {
logger.error("Failed to respond to notification request.", e);
GB.toast("Failed to respond to notification request:" + e.getMessage(), Toast.LENGTH_LONG, GB.WARN);
}
}
private byte[] getImageData(String sourceAppId) {
byte[] imageData = appIconCache.get(sourceAppId);
if (imageData == null) {
NotificationSpec notificationSpec = NotificationProvider.getInstance(support).getNotificationSpecForSourceAppId(sourceAppId);
if (notificationSpec != null) {
int iconId = notificationSpec.iconId;
try {
Drawable icon = null;
if (notificationSpec.iconId != 0) {
Context sourcePackageContext = support.getContext().createPackageContext(sourceAppId, 0);
icon = sourcePackageContext.getResources().getDrawable(notificationSpec.iconId);
}
if (icon == null) {
PackageManager pm = support.getContext().getPackageManager();
icon = pm.getApplicationIcon(sourceAppId);
}
imageData = IconHelper.getIconBytesFromDrawable(icon);
appIconCache.put(sourceAppId, imageData);
} catch (PackageManager.NameNotFoundException e) {
logger.error("Error while updating notification icons", e);
imageData = new byte[0];
}
} else {
imageData = new byte[0];
}
}
return imageData;
}
}

View File

@ -0,0 +1,38 @@
/* Copyright (C) 2021 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 <http://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.communication.message.ExpectedResponse;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.Message;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message.WithingsMessageType;
public class SyncRequestHandler implements IncomingMessageHandler {
private final WithingsSteelHRDeviceSupport support;
public SyncRequestHandler(WithingsSteelHRDeviceSupport support) {
this.support = support;
}
@Override
public void handleMessage(Message message) {
support.sendToDevice(new WithingsMessage(WithingsMessageType.SYNC_RESPONSE));
support.doSync();
}
}

View File

@ -0,0 +1,45 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
public final class AncsConstants {
public static final byte EVENT_ID_NOTIFICATION_ADDED = 0;
public static final byte EVENT_ID_NOTIFICATION_MODIFIED = 1;
public static final byte EVENT_ID_NOTIFICATION_REMOVED = 2;
public static final byte EVENT_FLAGS_SILENT = (1 << 0);
public static final byte EVENT_FLAGS_IMPORTANT = (1 << 1);
public static final byte EVENT_FLAGS_PREEXISTING = (1 << 2);
public static final byte EVENT_FLAGS_POSITIVE_ACTION = (1 << 3);
public static final byte EVENT_FLAGS_NEGATIVE_ACTION = (1 << 4);
public static final byte CATEGORY_ID_OTHER = 0;
public static final byte CATEGORY_ID_INCOMING_CALL = 1;
public static final byte CATEGORY_ID_MISSED_CALL = 2;
public static final byte CATEGORY_ID_VOICEMAIL = 3;
public static final byte CATEGORY_ID_SOCIAL = 4;
public static final byte CATEGORY_ID_SCHEDULE = 5;
public static final byte CATEGORY_ID_EMAIL = 6;
public static final byte CATEGORY_ID_NEWS = 7;
public static final byte CATEGORY_ID_HEALTHANDFITNESS = 8;
public static final byte CATEGORY_ID_BUSINESSANDFINANCE = 9;
public static final byte CATEGORY_ID_LOCATION = 10;
public static final byte CATEGORY_ID_ENTERTAINMENT = 11;
private AncsConstants(){}
}

View File

@ -0,0 +1,76 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
public class GetNotificationAttributes {
private byte commandID;
private int notificationUID;
private List<RequestedNotificationAttribute> attributes = new ArrayList<>();
public byte getCommandID() {
return commandID;
}
public void setCommandID(byte commandID) {
this.commandID = commandID;
}
public int getNotificationUID() {
return notificationUID;
}
public void setNotificationUID(int notificationUID) {
this.notificationUID = notificationUID;
}
public List<RequestedNotificationAttribute> getAttributes() {
return Collections.unmodifiableList(attributes);
}
public void addAttribute(RequestedNotificationAttribute attribute) {
attributes.add(attribute);
}
public void deserialize(byte[] rawData) {
ByteBuffer buffer = ByteBuffer.wrap(rawData);
commandID = buffer.get();
notificationUID = buffer.getInt();
while (buffer.hasRemaining()) {
RequestedNotificationAttribute requestedNotificationAttribute = new RequestedNotificationAttribute();
int length = 1;
if (buffer.remaining() >= 3) {
length = 3;
}
byte[] rawAttributeData = new byte[length];
buffer.get(rawAttributeData);
requestedNotificationAttribute.deserialize(rawAttributeData);
attributes.add(requestedNotificationAttribute);
}
}
public byte[] serialize() {
return new byte[0];
}
}

View File

@ -0,0 +1,57 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
public class GetNotificationAttributesResponse {
private byte commandID = 0;
private int notificationUID;
private List<NotificationAttribute> attributes = new ArrayList<>();
public GetNotificationAttributesResponse(int notificationUID) {
this.notificationUID = notificationUID;
}
public void addAttribute(NotificationAttribute attribute) {
attributes.add(attribute);
}
public byte[] serialize() {
ByteBuffer buffer = ByteBuffer.allocate(getLength());
buffer.put(commandID);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putInt(notificationUID);
buffer.order(ByteOrder.BIG_ENDIAN);
for (NotificationAttribute attribute : attributes) {
buffer.put(attribute.serialize());
}
return buffer.array();
}
private int getLength() {
int length = 5;
for (NotificationAttribute attribute : attributes) {
length += attribute.getAttributeLength() + 3;
}
return length;
}
}

View File

@ -0,0 +1,88 @@
/* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.util.GB;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class NotificationAttribute {
private byte attributeID;
private short attributeLength;
private short attributeMaxLength;
private String value;
public void setAttributeMaxLength(short attributeMaxLength) {
this.attributeMaxLength = attributeMaxLength;
}
public byte getAttributeID() {
return attributeID;
}
public void setAttributeID(byte attributeID) {
this.attributeID = attributeID;
}
public short getAttributeLength() {
short length = (short)(value != null? value.getBytes(StandardCharsets.UTF_8).length : 0);
if (attributeMaxLength > 0 && length > attributeMaxLength) {
length = attributeMaxLength;
}
return length;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public byte[] serialize() {
attributeLength = getAttributeLength();
int length = attributeLength + 3;
ByteBuffer buffer = ByteBuffer.allocate(length);
buffer.put(attributeID);
buffer.order(ByteOrder.LITTLE_ENDIAN);
buffer.putShort(attributeLength);
if (value != null) {
buffer.order(ByteOrder.BIG_ENDIAN);
buffer.put(value.getBytes(StandardCharsets.UTF_8), 0, attributeLength);
}
return buffer.array();
}
public void deserialize(byte[] rawData) {
ByteBuffer buffer = ByteBuffer.wrap(rawData);
attributeID = buffer.get();
buffer.order(ByteOrder.LITTLE_ENDIAN);
attributeLength = buffer.getShort();
buffer.order(ByteOrder.BIG_ENDIAN);
if (attributeLength > 0) {
byte[] rawValue = new byte[attributeLength];
buffer.get(rawValue);
value = new String(rawValue, StandardCharsets.UTF_8);
}
}
}

Some files were not shown because too many files have changed in this diff Show More