From c1fd0b77adbc29ead50fdf6dd2fd1a3ec9d6f99e Mon Sep 17 00:00:00 2001 From: hrglpfrmpf Date: Wed, 26 Jul 2023 17:20:43 +0000 Subject: [PATCH] Support for Withings Steel HR (#2831) Co-authored-by: hrglpfrmpf Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2831 Co-authored-by: hrglpfrmpf Co-committed-by: hrglpfrmpf --- .../gadgetbridge/daogen/GBDaoGenerator.java | 17 +- app/src/main/AndroidManifest.xml | 4 + .../withingssteelhr/RotaryControl.java | 201 +++++ .../WithingsCalibrationActivity.java | 159 ++++ .../WithingsSteelHRDeviceCoordinator.java | 175 +++++ .../WithingsSteelHRSampleProvider.java | 110 +++ .../gadgetbridge/model/DeviceType.java | 1 + .../service/DeviceSupportFactory.java | 3 + .../service/btle/TransactionBuilder.java | 14 + .../AuthenticationHandler.java | 128 ++++ .../devices/withingssteelhr/IconHelper.java | 71 ++ .../WithingsSteelHRDeviceSupport.java | 725 ++++++++++++++++++ .../activity/ActivityEntry.java | 101 +++ .../activity/SleepActivitySampleHelper.java | 93 +++ .../activity/WithingsActivityType.java | 168 ++++ .../communication/WithingsServerAction.java | 43 ++ .../communication/WithingsUUID.java | 34 + .../conversation/AbstractConversation.java | 107 +++ .../conversation/AbstractResponseHandler.java | 30 + .../conversation/ActivitySampleHandler.java | 269 +++++++ .../conversation/BatteryStateHandler.java | 57 ++ .../conversation/Conversation.java | 28 + .../conversation/ConversationObserver.java | 21 + .../conversation/ConversationQueue.java | 100 +++ .../conversation/HeartRateHandler.java | 87 +++ .../conversation/ResponseHandler.java | 23 + .../conversation/SetupFinishedHandler.java | 35 + .../conversation/SimpleConversation.java | 42 + .../conversation/SyncFinishedHandler.java | 37 + .../WorkoutScreenListHandler.java | 64 ++ .../datastructures/ActivityHeartrate.java | 47 ++ .../ActivitySampleCalories.java | 54 ++ .../ActivitySampleCalories2.java | 25 + .../ActivitySampleDuration.java | 51 ++ .../ActivitySampleMovement.java | 69 ++ .../datastructures/ActivitySampleRun.java | 36 + .../datastructures/ActivitySampleSleep.java | 50 ++ .../datastructures/ActivitySampleSwim.java | 36 + .../datastructures/ActivitySampleTime.java | 52 ++ .../datastructures/ActivitySampleUnknown.java | 36 + .../datastructures/ActivitySampleWalk.java | 50 ++ .../datastructures/ActivityTarget.java | 51 ++ .../datastructures/AlarmName.java | 44 ++ .../datastructures/AlarmSettings.java | 111 +++ .../datastructures/AlarmStatus.java | 51 ++ .../datastructures/AncsStatus.java | 45 ++ .../datastructures/BatteryValues.java | 73 ++ .../datastructures/Challenge.java | 80 ++ .../datastructures/ChallengeResponse.java | 54 ++ .../datastructures/DataStructureFactory.java | 168 ++++ .../datastructures/EndOfTransmission.java | 44 ++ .../datastructures/GetActivitySamples.java | 47 ++ .../communication/datastructures/GlyphId.java | 50 ++ .../datastructures/HeartRate.java | 47 ++ .../datastructures/ImageData.java | 47 ++ .../datastructures/ImageMetaData.java | 66 ++ .../datastructures/LiveHeartRate.java | 48 ++ .../datastructures/LiveWorkoutEnd.java | 50 ++ .../datastructures/LiveWorkoutPauseState.java | 66 ++ .../datastructures/LiveWorkoutStart.java | 50 ++ .../communication/datastructures/Locale.java | 44 ++ .../datastructures/MoveHand.java | 49 ++ .../communication/datastructures/Probe.java | 51 ++ .../datastructures/ProbeOsVersion.java | 43 ++ .../datastructures/ProbeReply.java | 111 +++ .../datastructures/ScreenSettings.java | 67 ++ .../datastructures/SourceAppId.java | 52 ++ .../communication/datastructures/Status.java | 36 + .../communication/datastructures/Time.java | 102 +++ .../datastructures/TypeVersion.java | 39 + .../communication/datastructures/User.java | 102 +++ .../datastructures/UserSecret.java | 39 + .../datastructures/UserUnit.java | 52 ++ .../datastructures/UserUnitConstants.java | 29 + .../datastructures/WithingsStructure.java | 103 +++ .../datastructures/WithingsStructureType.java | 72 ++ .../datastructures/WorkoutGpsState.java | 43 ++ .../datastructures/WorkoutScreen.java | 67 ++ .../datastructures/WorkoutScreenData.java | 52 ++ .../datastructures/WorkoutScreenList.java | 48 ++ .../datastructures/WorkoutType.java | 50 ++ .../message/AbstractMessage.java | 98 +++ .../message/ExpectedResponse.java | 23 + .../message/GlyphRequestHandler.java | 92 +++ .../communication/message/Message.java | 36 + .../communication/message/MessageBuilder.java | 89 +++ .../communication/message/MessageFactory.java | 55 ++ .../message/SimpleHexToByteMessage.java | 71 ++ .../message/WithingsMessage.java | 64 ++ .../message/WithingsMessageType.java | 68 ++ .../incoming/IncomingMessageHandler.java | 23 + .../IncomingMessageHandlerFactory.java | 86 +++ .../incoming/LiveHeartrateHandler.java | 88 +++ .../message/incoming/LiveWorkoutHandler.java | 147 ++++ .../incoming/NotificationRequestHandler.java | 101 +++ .../message/incoming/SyncRequestHandler.java | 38 + .../notification/AncsConstants.java | 45 ++ .../GetNotificationAttributes.java | 76 ++ .../GetNotificationAttributesResponse.java | 57 ++ .../notification/NotificationAttribute.java | 88 +++ .../notification/NotificationProvider.java | 184 +++++ .../notification/NotificationSource.java | 55 ++ .../RequestedNotificationAttribute.java | 58 ++ .../gadgetbridge/util/DeviceHelper.java | 3 +- .../layout/activity_withings_calibration.xml | 58 ++ app/src/main/res/values/arrays.xml | 87 +++ app/src/main/res/values/rc_attrs.xml | 11 + app/src/main/res/values/strings.xml | 25 + .../xml/devicesettings_withingssteelhr.xml | 22 + .../DeviceCommunicationServiceTestCase.java | 8 +- .../communication/MessageBuilderTest.java | 151 ++++ .../datastructures/ChallengeTest.java | 67 ++ .../DataStructureFactoryTest.java | 133 ++++ .../datastructures/ProbeReplyTest.java | 32 + .../datastructures/TimeTest.java | 33 + .../datastructures/WithingsTestStructure.java | 30 + .../message/AbstractMessageTest.java | 60 ++ .../NotificationAttributeTest.java | 56 ++ .../RequestedNotificationAttributeTest.java | 29 + 119 files changed, 8337 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/RotaryControl.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsCalibrationActivity.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRDeviceCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRSampleProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/IconHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/ActivityEntry.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsServerAction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUID.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractConversation.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/Conversation.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationObserver.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ResponseHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SimpleConversation.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityHeartrate.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories2.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleDuration.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleMovement.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleRun.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSleep.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSwim.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleTime.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleUnknown.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleWalk.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityTarget.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmName.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmSettings.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmStatus.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AncsStatus.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/BatteryValues.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Challenge.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeResponse.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/EndOfTransmission.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GetActivitySamples.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GlyphId.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/HeartRate.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageData.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageMetaData.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveHeartRate.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutEnd.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutPauseState.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutStart.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Locale.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/MoveHand.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Probe.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeOsVersion.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReply.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/SourceAppId.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Status.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Time.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TypeVersion.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/User.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserSecret.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnit.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnitConstants.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructure.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutGpsState.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreen.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenData.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenList.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/ExpectedResponse.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/Message.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/SimpleHexToByteMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessage.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/AncsConstants.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributes.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributesResponse.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttribute.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationSource.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttribute.java create mode 100644 app/src/main/res/layout/activity_withings_calibration.xml create mode 100644 app/src/main/res/values/rc_attrs.xml create mode 100644 app/src/main/res/xml/devicesettings_withingssteelhr.xml create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/MessageBuilderTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactoryTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReplyTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TimeTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsTestStructure.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessageTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttributeTest.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttributeTest.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 1967fbaea..112a5349d 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -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; + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a6c8f4f9b..919a0f358 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -722,6 +722,10 @@ android:name=".devices.qhybrid.CalibrationActivity" android:label="@string/qhybrid_title_calibration" android:parentActivityName=".devices.qhybrid.HRConfigActivity" /> + . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsCalibrationActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsCalibrationActivity.java new file mode 100644 index 000000000..f46d1cd82 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsCalibrationActivity.java @@ -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 . */ +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 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); + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRDeviceCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRDeviceCoordinator.java new file mode 100644 index 000000000..f964e0272 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRDeviceCoordinator.java @@ -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 . */ +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 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 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRSampleProvider.java new file mode 100644 index 000000000..393422a98 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/withingssteelhr/WithingsSteelHRSampleProvider.java @@ -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 . */ +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 { + private static final Logger logger = LoggerFactory.getLogger(WithingsSteelHRSampleProvider.class); + + public WithingsSteelHRSampleProvider(GBDevice device, DaoSession session) { + super(device, session); + } + + @Override + public AbstractDao 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 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 008108190..d871ec78c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 54da7a7e2..99e0bb332 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -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()); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java index 0e3b811e7..d6e43f701 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/TransactionBuilder.java @@ -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) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java new file mode 100644 index 000000000..1bc1a3ab8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/AuthenticationHandler.java @@ -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 . */ +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 getTypeFromReply(Class type, Message message) { + for (WithingsStructure structure : message.getDataStructures()) { + if (type.isInstance(structure)) { + return (T)structure; + } + } + + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/IconHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/IconHelper.java new file mode 100644 index 000000000..8203b1e59 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/IconHelper.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java new file mode 100644 index 000000000..00ad34dd2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/WithingsSteelHRDeviceSupport.java @@ -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 . */ +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 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 allActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_values)); + final List defaultActivityTypes = Arrays.asList(getContext().getResources().getStringArray(R.array.pref_withings_steel_activity_types_default)); + final String activityTypesPref = prefs.getString("workout_activity_types_sortable", null); + + final List enabledActivityTypes; + if (activityTypesPref == null || activityTypesPref.equals("")) { + enabledActivityTypes = defaultActivityTypes; + } else { + enabledActivityTypes = Arrays.asList(activityTypesPref.split(",")); + } + + conversationQueue.clear(); + for (int i = 0; i < enabledActivityTypes.size(); i++) { + String workoutType = enabledActivityTypes.get(i); + try { + Message message = createWorkoutScreenMessage(workoutType); + if (i == enabledActivityTypes.size() - 1) { + message.addDataStructure(new EndOfTransmission()); + } + addSimpleConversationToQueue(message); + } catch (Exception e) { + 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; + } + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/ActivityEntry.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/ActivityEntry.java new file mode 100644 index 000000000..0b8e99638 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/ActivityEntry.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java new file mode 100644 index 000000000..706f5e12b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/SleepActivitySampleHelper.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java new file mode 100644 index 000000000..f6ce0770b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/activity/WithingsActivityType.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsServerAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsServerAction.java new file mode 100644 index 000000000..438a71a15 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsServerAction.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUID.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUID.java new file mode 100644 index 000000000..064f90fcb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/WithingsUUID.java @@ -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 . */ +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() {} +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractConversation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractConversation.java new file mode 100644 index 000000000..9c3231a7d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractConversation.java @@ -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 . */ +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 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 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); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java new file mode 100644 index 000000000..0549afc21 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/AbstractResponseHandler.java @@ -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 . */ +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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java new file mode 100644 index 000000000..4bce8cdef --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ActivitySampleHandler.java @@ -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 . */ +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 activityEntries = new ArrayList<>(); + private List heartrateEntries = new ArrayList<>(); + + public ActivitySampleHandler(WithingsSteelHRDeviceSupport support) { + super(support); + } + + @Override + public void handleResponse(Message response) { + List data = response.getDataStructures(); + if (data != null) { + handleActivityData(data, response.getType()); + } + } + + public void onSyncFinished() { + mergeHeartrateSamplesIntoActivitySammples(); + saveData(); + } + + private void handleActivityData(List 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 activitySamples = new ArrayList<>(); + for (ActivityEntry activityEntry : activityEntries) { + convertToSampleAndAddToList(activitySamples, activityEntry); + } + for (ActivityEntry activityEntry : heartrateEntries) { + convertToSampleAndAddToList(activitySamples, activityEntry); + } + + writeToDB(activitySamples); + } + + private void writeToDB(List activitySamples) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); + Long deviceId = DBHelper.getDevice(device, dbHandler.getDaoSession()).getId(); + WithingsSteelHRSampleProvider provider = new WithingsSteelHRSampleProvider(device, dbHandler.getDaoSession()); + for (WithingsSteelHRActivitySample sample : activitySamples) { + sample.setDeviceId(deviceId); + sample.setUserId(userId); + } + 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 activitySamples, ActivityEntry activityEntry) { + WithingsSteelHRActivitySample sample = new WithingsSteelHRActivitySample(); + sample.setTimestamp(activityEntry.getTimestamp()); + sample.setDuration(activityEntry.getDuration()); + sample.setHeartRate(activityEntry.getHeartrate()); + sample.setSteps(activityEntry.getSteps()); + sample.setRawKind(activityEntry.getRawKind()); + sample.setCalories(activityEntry.getCalories()); + sample.setDistance(activityEntry.getDistance()); + sample.setRawIntensity(activityEntry.getRawIntensity()); + activitySamples.add(sample); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java new file mode 100644 index 000000000..d7d15ca8e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/BatteryStateHandler.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/Conversation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/Conversation.java new file mode 100644 index 000000000..5cc7f283d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/Conversation.java @@ -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 . */ +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(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationObserver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationObserver.java new file mode 100644 index 000000000..73f833d92 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationObserver.java @@ -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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.conversation; + +public interface ConversationObserver { + void onConversationCompleted(short conversationType); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java new file mode 100644 index 000000000..b4e49376c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ConversationQueue.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java new file mode 100644 index 000000000..d9f9c664d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/HeartRateHandler.java @@ -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 . */ +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()); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ResponseHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ResponseHandler.java new file mode 100644 index 000000000..096c6d64d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/ResponseHandler.java @@ -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 . */ +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); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java new file mode 100644 index 000000000..5f7945464 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SetupFinishedHandler.java @@ -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 . */ +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();; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SimpleConversation.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SimpleConversation.java new file mode 100644 index 000000000..ca5e42228 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SimpleConversation.java @@ -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 . */ +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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java new file mode 100644 index 000000000..fa98e90f2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/SyncFinishedHandler.java @@ -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 . */ +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(); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java new file mode 100644 index 000000000..fad7ad734 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/conversation/WorkoutScreenListHandler.java @@ -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 . */ +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 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 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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityHeartrate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityHeartrate.java new file mode 100644 index 000000000..457e474ff --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityHeartrate.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories.java new file mode 100644 index 000000000..71da171ee --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories2.java new file mode 100644 index 000000000..da61199d9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleCalories2.java @@ -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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.datastructures; + +public class ActivitySampleCalories2 extends ActivitySampleCalories { + + @Override + public short getType() { + return WithingsStructureType.ACTIVITY_SAMPLE_CALORIES_2; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleDuration.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleDuration.java new file mode 100644 index 000000000..4b23da7fd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleDuration.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleMovement.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleMovement.java new file mode 100644 index 000000000..63df8f54c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleMovement.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleRun.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleRun.java new file mode 100644 index 000000000..2a1ae803f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleRun.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSleep.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSleep.java new file mode 100644 index 000000000..9df227e52 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSleep.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSwim.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSwim.java new file mode 100644 index 000000000..efb3e32b1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleSwim.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleTime.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleTime.java new file mode 100644 index 000000000..baaf95b9f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleTime.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleUnknown.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleUnknown.java new file mode 100644 index 000000000..03ccfd696 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleUnknown.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleWalk.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleWalk.java new file mode 100644 index 000000000..d68771148 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivitySampleWalk.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityTarget.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityTarget.java new file mode 100644 index 000000000..d4a9bf4e9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ActivityTarget.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmName.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmName.java new file mode 100644 index 000000000..c2e05daef --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmName.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmSettings.java new file mode 100644 index 000000000..bb19b062b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmSettings.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmStatus.java new file mode 100644 index 000000000..708405be2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AlarmStatus.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AncsStatus.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AncsStatus.java new file mode 100644 index 000000000..0b95df5b5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/AncsStatus.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/BatteryValues.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/BatteryValues.java new file mode 100644 index 000000000..adfeee192 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/BatteryValues.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Challenge.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Challenge.java new file mode 100644 index 000000000..a9709e052 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Challenge.java @@ -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 . */ +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; + } + + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeResponse.java new file mode 100644 index 000000000..5fb86b32f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ChallengeResponse.java @@ -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 . */ +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; + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java new file mode 100644 index 000000000..89a4b591c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/DataStructureFactory.java @@ -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 . */ +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 createStructuresFromRawData(byte[] rawData) { + List structures = new ArrayList<>(); + if (rawData == null) { + return structures; + } + + List 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 splitRawData(byte[] rawData) { + int remainingBytes = rawData.length; + List 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/EndOfTransmission.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/EndOfTransmission.java new file mode 100644 index 000000000..d969885f7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/EndOfTransmission.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GetActivitySamples.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GetActivitySamples.java new file mode 100644 index 000000000..48d114ad8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GetActivitySamples.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GlyphId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GlyphId.java new file mode 100644 index 000000000..0cc68df00 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/GlyphId.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/HeartRate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/HeartRate.java new file mode 100644 index 000000000..2d4736b16 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/HeartRate.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageData.java new file mode 100644 index 000000000..d2e1123e7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageData.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageMetaData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageMetaData.java new file mode 100644 index 000000000..847e9e911 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ImageMetaData.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveHeartRate.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveHeartRate.java new file mode 100644 index 000000000..dfe801428 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveHeartRate.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutEnd.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutEnd.java new file mode 100644 index 000000000..e5694a7b9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutEnd.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutPauseState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutPauseState.java new file mode 100644 index 000000000..e919432c7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutPauseState.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutStart.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutStart.java new file mode 100644 index 000000000..f59d648c3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/LiveWorkoutStart.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Locale.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Locale.java new file mode 100644 index 000000000..e3e31a9d1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Locale.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/MoveHand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/MoveHand.java new file mode 100644 index 000000000..be82c1e54 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/MoveHand.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Probe.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Probe.java new file mode 100644 index 000000000..904f1caaf --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Probe.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeOsVersion.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeOsVersion.java new file mode 100644 index 000000000..da5adbf5b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeOsVersion.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReply.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReply.java new file mode 100644 index 000000000..07ef568ec --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ProbeReply.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java new file mode 100644 index 000000000..22c6f438f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/ScreenSettings.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/SourceAppId.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/SourceAppId.java new file mode 100644 index 000000000..98892a599 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/SourceAppId.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Status.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Status.java new file mode 100644 index 000000000..861a6a1d5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Status.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Time.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Time.java new file mode 100644 index 000000000..e7d41f2d7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/Time.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TypeVersion.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TypeVersion.java new file mode 100644 index 000000000..5a5156986 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/TypeVersion.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/User.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/User.java new file mode 100644 index 000000000..950a89ab8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/User.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserSecret.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserSecret.java new file mode 100644 index 000000000..a030fb267 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserSecret.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnit.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnit.java new file mode 100644 index 000000000..2adc4ebce --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnit.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnitConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnitConstants.java new file mode 100644 index 000000000..7f4e38233 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/UserUnitConstants.java @@ -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 . */ +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() {} +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructure.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructure.java new file mode 100644 index 000000000..9e94d852f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructure.java @@ -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 . */ +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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java new file mode 100644 index 000000000..14167c550 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WithingsStructureType.java @@ -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 . */ +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() {} +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutGpsState.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutGpsState.java new file mode 100644 index 000000000..9535e7e5a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutGpsState.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreen.java new file mode 100644 index 000000000..d7d5251d7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreen.java @@ -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 . */ +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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenData.java new file mode 100644 index 000000000..3e7961f5c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenData.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenList.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenList.java new file mode 100644 index 000000000..8918cd038 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutScreenList.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutType.java new file mode 100644 index 000000000..b33781c66 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/datastructures/WorkoutType.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessage.java new file mode 100644 index 000000000..04e990b4e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/AbstractMessage.java @@ -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 . */ +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 dataStructures = new ArrayList(); + + public List 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 getStructureByType(Class 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/ExpectedResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/ExpectedResponse.java new file mode 100644 index 000000000..55c186b4a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/ExpectedResponse.java @@ -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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.message; + +public enum ExpectedResponse { + NONE, + SIMPLE, + EOT +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java new file mode 100644 index 000000000..48c91dc01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/GlyphRequestHandler.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/Message.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/Message.java new file mode 100644 index 000000000..6e73ca786 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/Message.java @@ -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 . */ +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 getDataStructures(); + void addDataStructure(WithingsStructure data); + short getType(); + byte[] getRawData(); + boolean needsResponse(); + boolean needsEOT(); + boolean isIncomingMessage(); + T getStructureByType(Class type); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java new file mode 100644 index 000000000..eccd5039c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageBuilder.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java new file mode 100644 index 000000000..2a092c5c5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/MessageFactory.java @@ -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 . */ +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 structures = dataStructureFactory.createStructuresFromRawData(rawStructureData); + for (WithingsStructure structure : structures) { + message.addDataStructure(structure); + } + + return message; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/SimpleHexToByteMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/SimpleHexToByteMessage.java new file mode 100644 index 000000000..7a713c05f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/SimpleHexToByteMessage.java @@ -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 . */ +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 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 getStructureByType(Class type) { + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessage.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessage.java new file mode 100644 index 000000000..8a789b834 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessage.java @@ -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 . */ +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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java new file mode 100644 index 000000000..e05fa31a4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/WithingsMessageType.java @@ -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 . */ +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() {} +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandler.java new file mode 100644 index 000000000..508ab2257 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandler.java @@ -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 . */ +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); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java new file mode 100644 index 000000000..5a7465411 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/IncomingMessageHandlerFactory.java @@ -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 . */ +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 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()); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java new file mode 100644 index 000000000..8baee5bd4 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveHeartrateHandler.java @@ -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 . */ +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 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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java new file mode 100644 index 000000000..28ccf7de7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/LiveWorkoutHandler.java @@ -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 . */ +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 data = message.getDataStructures(); + if (data != null) { + handleLiveData(data); + } + } + + private void handleLiveData(List 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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java new file mode 100644 index 000000000..f36d3d217 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/NotificationRequestHandler.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java new file mode 100644 index 000000000..90a436ada --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/message/incoming/SyncRequestHandler.java @@ -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 . */ +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(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/AncsConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/AncsConstants.java new file mode 100644 index 000000000..b061ac3c2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/AncsConstants.java @@ -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 . */ +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(){} +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributes.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributes.java new file mode 100644 index 000000000..760c17a11 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributes.java @@ -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 . */ +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 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 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]; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributesResponse.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributesResponse.java new file mode 100644 index 000000000..14b7a8bd7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/GetNotificationAttributesResponse.java @@ -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 . */ +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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttribute.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttribute.java new file mode 100644 index 000000000..f5bbdee46 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationAttribute.java @@ -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 . */ +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); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java new file mode 100644 index 000000000..fe3ab81eb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationProvider.java @@ -0,0 +1,184 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.WithingsSteelHRDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class NotificationProvider { + + private static final Logger logger = LoggerFactory.getLogger(NotificationProvider.class); + private final WithingsSteelHRDeviceSupport support; + private final Map pendingNotifications = new HashMap<>(); + private static NotificationProvider instance; + + public static NotificationProvider getInstance(WithingsSteelHRDeviceSupport support) { + if (instance == null) { + instance = new NotificationProvider(support); + } + + return instance; + } + + private NotificationProvider(WithingsSteelHRDeviceSupport support) { + this.support = support; + } + + public void notifyClient(NotificationSpec spec) { + NotificationSource notificationSource = new NotificationSource(spec.getId(), + AncsConstants.EVENT_ID_NOTIFICATION_ADDED, + AncsConstants.EVENT_FLAGS_IMPORTANT, + mapNotificationType(spec.type), + (byte)1); + pendingNotifications.put(notificationSource.getNotificationUID(), spec); + support.sendAncsNotificationSourceNotification(notificationSource); + } + + public void handleNotificationAttributeRequest(GetNotificationAttributes request) { + logger.debug("Request has ID: " + request.getNotificationUID()); + NotificationSpec spec = pendingNotifications.get(request.getNotificationUID()); + if (spec == null) { + logger.info("No pending notification with notificationUID " + request.getNotificationUID()); + NotificationSource notificationSource = new NotificationSource(request.getNotificationUID(), + AncsConstants.EVENT_ID_NOTIFICATION_REMOVED, + AncsConstants.EVENT_FLAGS_IMPORTANT, + (byte)0, + (byte)1); + support.sendAncsNotificationSourceNotification(notificationSource); + return; + } + + GetNotificationAttributesResponse response = new GetNotificationAttributesResponse(request.getNotificationUID()); + List requestedAttributes = request.getAttributes(); + logger.debug(requestedAttributes.size() + " attributes requested."); + + boolean complete = false; + + for (RequestedNotificationAttribute requestedAttribute : requestedAttributes) { + NotificationAttribute attribute = new NotificationAttribute(); + attribute.setAttributeID(requestedAttribute.getAttributeID()); + attribute.setAttributeMaxLength(requestedAttribute.getAttributeMaxLength()); + logger.debug("Handling attribute " + attribute.getAttributeID() + " with maxLength " + attribute.getAttributeLength()); + String value = ""; + if (requestedAttribute.getAttributeID() == 0) { + value = spec.sourceAppId; + } + if (requestedAttribute.getAttributeID() == 1) { + complete = true; + value = spec.sender != null? spec.sender : (spec.phoneNumber != null? spec.phoneNumber : (spec.sourceName != null? spec.sourceName : "Unknown")); + } + if (requestedAttribute.getAttributeID() == 2) { + complete = true; + value = spec.title != null? spec.title : (spec.subject != null? spec.subject : " "); + } + if (requestedAttribute.getAttributeID() == 3) { + complete = true; + value = (spec.body != null? spec.body : " "); + } + + if (value != null) { + // Remove linefeed and carriage returns as the watch cannot display this: + value = value.replace("\n", " "); + value = value.replace("\r", " "); + if (requestedAttribute.getAttributeMaxLength() == 0 || requestedAttribute.getAttributeMaxLength() >= value.length()) { + attribute.setValue(value); + } else { + attribute.setValue(value.substring(0, requestedAttribute.getAttributeMaxLength())); + } + } + + logger.debug("Sending attribute " + attribute.getAttributeID() + " with value " + attribute.getValue()); + response.addAttribute(attribute); + } + + support.sendAncsDataSourceNotification(response); + + if (complete) { + pendingNotifications.remove(request.getNotificationUID()); + } + } + + public NotificationSpec getNotificationSpecForSourceAppId(String sourceAppId) { + for (NotificationSpec notificationSpec : pendingNotifications.values()) { + if (notificationSpec.sourceAppId != null && notificationSpec.sourceAppId.equalsIgnoreCase(sourceAppId)) { + return notificationSpec; + } + } + + return null; + } + + private byte mapNotificationType(NotificationType type) { + switch (type) { + case GENERIC_ALARM_CLOCK: + case BUSINESS_CALENDAR: + case GENERIC_CALENDAR: + return AncsConstants.CATEGORY_ID_SCHEDULE; + case GENERIC_EMAIL: + case YAHOO_MAIL: + case GOOGLE_INBOX: + case GMAIL: + case OUTLOOK: + return AncsConstants.CATEGORY_ID_EMAIL; + case GENERIC_NAVIGATION: + return AncsConstants.CATEGORY_ID_LOCATION; + case GENERIC_PHONE: + return AncsConstants.CATEGORY_ID_INCOMING_CALL; + case MAILBOX: + return AncsConstants.CATEGORY_ID_MISSED_CALL; + case LINE: + case RIOT: + case SIGNAL: + case WIRE: + case SKYPE: + case SLACK: + case SNAPCHAT: + case TELEGRAM: + case THREEMA: + case KONTALK: + case ANTOX: + case DISCORD: + case TRANSIT: + case TWITTER: + case VIBER: + case WECHAT: + case WHATSAPP: + case FACEBOOK: + case FACEBOOK_MESSENGER: + case LINKEDIN: + case HIPCHAT: + case INSTAGRAM: + case KAKAO_TALK: + case GENERIC_SMS: + case GOOGLE_MESSENGER: + case GOOGLE_HANGOUTS: + return AncsConstants.CATEGORY_ID_SOCIAL; + default: + return AncsConstants.CATEGORY_ID_OTHER; + } + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationSource.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationSource.java new file mode 100644 index 000000000..e18476fb9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/NotificationSource.java @@ -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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification; + +import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.Random; + +public class NotificationSource { + private byte eventID; + private byte eventFlags; + private byte categoryId; + private byte categoryCount; + private int notificationUID; + + public NotificationSource(int notificationUID, byte eventID, byte eventFlags, byte categoryId, byte categoryCount) { + this.eventID = eventID; + this.eventFlags = eventFlags; + this.categoryId = categoryId; + this.categoryCount = categoryCount; + this.notificationUID = Integer.valueOf(new Random().nextInt()); + } + + public int getNotificationUID() { + return notificationUID; + } + + void setNotificationUID(int notificationUID) { + this.notificationUID = notificationUID; + } + + public byte[] serialize() { + ByteBuffer buffer = ByteBuffer.allocate(8); + buffer.put(eventID); + buffer.put(eventFlags); + buffer.put(categoryId); + buffer.put(categoryCount); + buffer.putInt(notificationUID); + return buffer.array(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttribute.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttribute.java new file mode 100644 index 000000000..ffd84229f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/withingssteelhr/communication/notification/RequestedNotificationAttribute.java @@ -0,0 +1,58 @@ +/* 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 . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.withingssteelhr.communication.notification; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class RequestedNotificationAttribute { + private byte attributeID; + private short attributeMaxLength; + + public byte getAttributeID() { + return attributeID; + } + + public void setAttributeID(byte attributeID) { + this.attributeID = attributeID; + } + + public short getAttributeMaxLength() { + return attributeMaxLength; + } + + public void setAttributeMaxLength(short attributeMaxLength) { + this.attributeMaxLength = attributeMaxLength; + } + + public byte[] serialize() { + ByteBuffer buffer = ByteBuffer.allocate(3); + buffer.order(ByteOrder.LITTLE_ENDIAN); + buffer.put(attributeID); + buffer.putShort(attributeMaxLength); + return buffer.array(); + } + + public void deserialize(byte[] rawData) { + ByteBuffer buffer = ByteBuffer.wrap(rawData); + buffer.order(ByteOrder.LITTLE_ENDIAN); + attributeID = buffer.get(); + if (buffer.capacity() >= 3) { + attributeMaxLength = buffer.getShort(); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index c16ef4461..7faffc1d8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -146,6 +146,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.withingssteelhr.WithingsSteelHRDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -379,7 +380,7 @@ public class DeviceHelper { result.add(new AsteroidOSDeviceCoordinator()); result.add(new SoFlowCoordinator()); result.add(new VivomoveHrCoordinator()); - + result.add(new WithingsSteelHRDeviceCoordinator()); return result; } diff --git a/app/src/main/res/layout/activity_withings_calibration.xml b/app/src/main/res/layout/activity_withings_calibration.xml new file mode 100644 index 000000000..c6fa3469e --- /dev/null +++ b/app/src/main/res/layout/activity_withings_calibration.xml @@ -0,0 +1,58 @@ + + + + + + + +