[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 <itcactus@noreply.codeberg.org>
Co-committed-by: ITCactus <itcactus@noreply.codeberg.org>
This commit is contained in:
ITCactus 2021-12-11 21:19:05 +01:00 committed by Andreas Shimokawa
parent dfde2c8bdf
commit 4cadb0412b
5 changed files with 226 additions and 6 deletions

View File

@ -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;
}
}

View File

@ -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<PineTimeActivitySample> {
private GBDevice mDevice;
private DaoSession mSession;
public PineTimeActivitySampleProvider(GBDevice device, DaoSession session) {
super(device, session);
mSession = session;
mDevice = device;
}
@Override
public AbstractDao<PineTimeActivitySample, ?> 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();
}
}

View File

@ -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");
}

View File

@ -68,12 +68,12 @@ public class PineTimeJFCoordinator extends AbstractDeviceCoordinator {
@Override
public boolean supportsActivityTracking() {
return false;
return true;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
return new PineTimeActivitySampleProvider(device, session);
}
@Override

View File

@ -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<PineTimeActivitySample> 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});
}
}