mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-24 17:45:50 +01:00
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:
parent
dfde2c8bdf
commit
4cadb0412b
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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});
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user