From 4cadb0412bc95116ed77875783b690a640cc8f11 Mon Sep 17 00:00:00 2001 From: ITCactus Date: Sat, 11 Dec 2021 21:19:05 +0100 Subject: [PATCH] [PineTime][2481] Steps/Activity sync support #2481 (#2486) added sync "steps" from PineTime/InfiniTime to Gadgetbridge. notes: * Steps sync works only since InfiniTime 1.7 * InfiniTime advertise "steps" info when the PineTime screen is ON (and a bit after that). hence: * you should unlock the PineTime screen before end of the day to not loose your latest progress (since the last unlock) at the end of the day; * when the PineTime screen is ON and you are moving, PineTime will send "steps" count every about 2-10 seconds, and Gadgetbridge may start to treat this data as an Activity (and also displaying it in Activity charts). that data and charts will not be accurate: you should wait for ["Health/Fitness data storage and expose to companion app](https://github.com/InfiniTimeOrg/InfiniTime/projects/4)" project to be implemented on the PineTime side. and meanwhile, in Gadgetbridge open "Device specific settings" and change/uncheck option in "Charts tabs" and "Activity info on device card" to leave only Steps data. Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2486 Co-authored-by: ITCactus Co-committed-by: ITCactus --- .../gadgetbridge/daogen/GBDaoGenerator.java | 12 +- .../PineTimeActivitySampleProvider.java | 71 +++++++++ .../devices/pinetime/PineTimeJFConstants.java | 6 + .../pinetime/PineTimeJFCoordinator.java | 4 +- .../devices/pinetime/PineTimeJFSupport.java | 139 +++++++++++++++++- 5 files changed, 226 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeActivitySampleProvider.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 870374c80..7f9432366 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -43,7 +43,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - Schema schema = new Schema(35, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(36, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -81,6 +81,7 @@ public class GBDaoGenerator { addBangleJSActivitySample(schema, user, device); addCasioGBX100Sample(schema, user, device); addFitProActivitySample(schema, user, device); + addPineTimeActivitySample(schema, user, device); addHybridHRActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -633,4 +634,13 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addPineTimeActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "PineTimeActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); + return activitySample; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeActivitySampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeActivitySampleProvider.java new file mode 100644 index 000000000..797a77ffa --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeActivitySampleProvider.java @@ -0,0 +1,71 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.pinetime; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import de.greenrobot.dao.AbstractDao; +import de.greenrobot.dao.Property; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.PineTimeActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.PineTimeActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class PineTimeActivitySampleProvider extends AbstractSampleProvider { + private GBDevice mDevice; + private DaoSession mSession; + + public PineTimeActivitySampleProvider(GBDevice device, DaoSession session) { + super(device, session); + + mSession = session; + mDevice = device; + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getPineTimeActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return PineTimeActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return PineTimeActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return PineTimeActivitySampleDao.Properties.DeviceId; + } + + @Override + public int normalizeType(int rawType) { + return rawType; + } + + @Override + public int toRawActivityKind(int activityKind) { + return activityKind; + } + + @Override + public float normalizeIntensity(int rawIntensity) { + return rawIntensity; + } + + /** + * Factory method to creates an empty sample of the correct type for this sample provider + * + * @return the newly created "empty" sample + */ + @Override + public PineTimeActivitySample createActivitySample() { + return new PineTimeActivitySample(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java index c15ac1c7b..d7943c2a9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java @@ -35,4 +35,10 @@ public class PineTimeJFConstants { public static final UUID UUID_CHARACTERISTICS_MUSIC_SHUFFLE = UUID.fromString("0000000c-78fc-48fe-8e23-433b3a1942d0"); public static final UUID UUID_CHARACTERISTIC_ALERT_NOTIFICATION_EVENT = UUID.fromString("00020001-78fc-48fe-8e23-433b3a1942d0"); + + // since 1.7. https://github.com/InfiniTimeOrg/InfiniTime/blob/develop/doc/MotionService.md + public static final UUID UUID_SERVICE_MOTION = UUID.fromString("00030000-78fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTIC_MOTION_STEP_COUNT = UUID.fromString("00030001-78fc-48fe-8e23-433b3a1942d0"); + public static final UUID UUID_CHARACTERISTIC_MOTION_RAW_XYZ_VALUES = UUID.fromString("00030002-78fc-48fe-8e23-433b3a1942d0"); + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java index d451ea698..12e153c45 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java @@ -68,12 +68,12 @@ public class PineTimeJFCoordinator extends AbstractDeviceCoordinator { @Override public boolean supportsActivityTracking() { - return false; + return true; } @Override public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { - return null; + return new PineTimeActivitySampleProvider(device, session); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java index 1359b612e..c2f29a689 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java @@ -23,37 +23,48 @@ import android.content.Intent; import android.net.Uri; import android.widget.Toast; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; - import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Calendar; import java.util.GregorianCalendar; +import java.util.List; import java.util.Locale; import java.util.UUID; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import no.nordicsemi.android.dfu.DfuLogListener; import no.nordicsemi.android.dfu.DfuProgressListener; import no.nordicsemi.android.dfu.DfuProgressListenerAdapter; import no.nordicsemi.android.dfu.DfuServiceController; import no.nordicsemi.android.dfu.DfuServiceInitiator; import no.nordicsemi.android.dfu.DfuServiceListenerHelper; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeActivitySampleProvider; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeDFUService; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeInstallHandler; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFConstants; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.entities.PineTimeActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.User; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceService; import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; @@ -85,6 +96,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL private int firmwareVersionMajor = 0; private int firmwareVersionMinor = 0; private int firmwareVersionPatch = 0; + /** * These are used to keep track when long strings haven't changed, * thus avoiding unnecessary transfers that are (potentially) very slow. @@ -220,6 +232,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE); addSupportedService(PineTimeJFConstants.UUID_SERVICE_MUSIC_CONTROL); addSupportedService(PineTimeJFConstants.UUID_CHARACTERISTIC_ALERT_NOTIFICATION_EVENT); + addSupportedService(PineTimeJFConstants.UUID_SERVICE_MOTION); IntentListener mListener = new IntentListener() { @Override @@ -454,9 +467,15 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL if (alertNotificationEventCharacteristic != null) { builder.notify(alertNotificationEventCharacteristic, true); } + + if (getSupportedServices().contains(PineTimeJFConstants.UUID_SERVICE_MOTION)) { + builder.notify(getCharacteristic(PineTimeJFConstants.UUID_CHARACTERISTIC_MOTION_STEP_COUNT), true); + builder.notify(getCharacteristic(PineTimeJFConstants.UUID_CHARACTERISTIC_MOTION_RAW_XYZ_VALUES), true); + } + setInitialized(builder); batteryInfoProfile.requestBatteryInfo(builder); - batteryInfoProfile.enableNotify(builder,true); + batteryInfoProfile.enableNotify(builder, true); return builder; } @@ -613,6 +632,14 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL } evaluateGBDeviceEvent(deviceEventCallControl); return true; + } else if (characteristicUUID.equals(PineTimeJFConstants.UUID_CHARACTERISTIC_MOTION_STEP_COUNT)) { + int steps = BLETypeConversions.toUint32(characteristic.getValue()); + if (LOG.isDebugEnabled()) { + GB.toast("Steps count: " + steps, Toast.LENGTH_SHORT, GB.INFO); + LOG.debug("onCharacteristicChanged: MotionService:Steps=" + steps); + } + onReceiveStepsSample(steps); + return true; } LOG.info("Unhandled characteristic changed: " + characteristicUUID); @@ -682,4 +709,110 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL public void onLogEvent(final String deviceAddress, final int level, final String message) { LOG.debug(message); } + + private void onReceiveStepsSample(int steps) { + this.onReceiveStepsSample((int) (Calendar.getInstance().getTimeInMillis() / 1000l), steps); + } + + private void onReceiveStepsSample(int timeStamp, int steps) { + PineTimeActivitySample sample = new PineTimeActivitySample(); + + int dayStepCount = this.getStepsOnDay(timeStamp); + int diff = steps - dayStepCount; + + if (diff > 0) { + LOG.debug("adding " + diff + " steps"); + + sample.setSteps(diff); + sample.setTimestamp(timeStamp); + + // since it's a local timestamp, it should NOT be treated as Activity because it will spoil activity charts + sample.setRawKind(ActivityKind.TYPE_UNKNOWN); + + this.addGBActivitySample(sample); + + Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES) + .putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample) + .putExtra(DeviceService.EXTRA_TIMESTAMP, sample.getTimestamp()); + LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent); + } + + } + + /** + * @param timeStamp Time stamp (in seconds) at some point during the requested day. + */ + private int getStepsOnDay(int timeStamp) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + + Calendar dayStart = new GregorianCalendar(); + Calendar dayEnd = new GregorianCalendar(); + + this.getDayStartEnd(timeStamp, dayStart, dayEnd); + + PineTimeActivitySampleProvider provider = new PineTimeActivitySampleProvider(this.getDevice(), dbHandler.getDaoSession()); + + List samples = provider.getAllActivitySamples( + (int) (dayStart.getTimeInMillis() / 1000L), + (int) (dayEnd.getTimeInMillis() / 1000L)); + + int totalSteps = 0; + + for (PineTimeActivitySample sample : samples) { + totalSteps += sample.getSteps(); + } + + return totalSteps; + + } catch (Exception ex) { + LOG.error(ex.getMessage()); + + return 0; + } + } + + /** + * @param timeStamp in seconds + */ + private void getDayStartEnd(int timeStamp, Calendar start, Calendar end) { + final int DAY = (24 * 60 * 60); + + int timeStampStart = ((timeStamp / DAY) * DAY); + int timeStampEnd = (timeStampStart + DAY); + + start.setTimeInMillis(timeStampStart * 1000L); + end.setTimeInMillis(timeStampEnd * 1000L); + } + + + private void addGBActivitySamples(PineTimeActivitySample[] samples) { + try (DBHandler dbHandler = GBApplication.acquireDB()) { + + User user = DBHelper.getUser(dbHandler.getDaoSession()); + Device device = DBHelper.getDevice(this.getDevice(), dbHandler.getDaoSession()); + + PineTimeActivitySampleProvider provider = new PineTimeActivitySampleProvider(this.getDevice(), dbHandler.getDaoSession()); + + for (PineTimeActivitySample sample : samples) { + sample.setDevice(device); + sample.setUser(user); + sample.setProvider(provider); + + sample.setRawIntensity(ActivitySample.NOT_MEASURED); + + provider.addGBActivitySample(sample); + } + + } catch (Exception ex) { + GB.toast(getContext(), "Error saving samples: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null, "Data transfer failed", false, 0, getContext()); + + LOG.error(ex.getMessage()); + } + } + + private void addGBActivitySample(PineTimeActivitySample sample) { + this.addGBActivitySamples(new PineTimeActivitySample[]{sample}); + } + }